├── .envrc ├── config ├── cli.sample.toml ├── config.sample.json └── main.go ├── types ├── action.go ├── subkey_algorithm.go ├── mq.go └── platform.go ├── controller ├── subkey_payload_test.go ├── proof_reupload.go ├── proof_exists_test.go ├── subkey_payload.go ├── main_test.go ├── proof_chain_single.go ├── proof_upload_test.go ├── main.go ├── subkey_submit.go ├── proof_chain.go ├── proof_chain_test.go ├── proof_exists.go ├── proof_payload_test.go ├── proof_payload.go ├── subkey_query.go └── proof_upload.go ├── .gitignore ├── common └── main.go ├── util ├── base1024 │ ├── base1024_emoji_alphabet.go │ ├── encode_test.go │ ├── decode_test.go │ ├── init.go │ ├── encode.go │ └── decode.go ├── util.go ├── sqs │ ├── sqs_test.go │ └── sqs.go └── crypto │ ├── crypto_test.go │ └── crypto.go ├── .editorconfig ├── docker-compose.yaml ├── cmd ├── lambda_headless │ ├── main.go │ └── Dockerfile ├── headless │ └── main.go ├── cli │ └── main.go ├── playground2 │ └── main.go ├── playground │ └── main.go ├── server │ └── main.go └── lambda │ └── main.go ├── model ├── main_test.go ├── avatar_alias_test.go ├── main.go ├── subkey_test.go ├── avatar_alias.go ├── proof.go ├── subkey.go └── proof_test.go ├── docs ├── scenario-on-chain-game.org └── proof_chain.md ├── test_build.sh ├── .github └── workflows │ ├── docker │ ├── Dockerfile │ └── Dockerfile.headless │ ├── PR.yaml │ ├── container.yaml │ ├── container_headless.yaml │ └── build.yaml ├── validator ├── twitter │ ├── api_test.go │ ├── api.go │ ├── twitter_test.go │ └── twitter.go ├── tiktok │ ├── fetcher_test.go │ ├── tiktok.go │ └── fetcher.go ├── activitypub │ ├── mastodon.go │ ├── activitypub_test.go │ └── misskey.go ├── steam │ └── test_76561198092541763.xml ├── nextid │ ├── nextid_test.go │ └── nextid.go ├── das │ ├── das_test.go │ └── das.go ├── keybase │ ├── keybase_test.go │ └── keybase.go ├── minds │ └── minds_test.go ├── discord │ ├── discord_test.go │ └── discord.go ├── github │ ├── github_test.go │ └── github.go ├── slack │ └── slack_test.go ├── ens │ └── ens_test.go ├── main.go ├── dns │ └── dns_test.go ├── telegram │ └── telegram_test.go ├── ethereum │ └── ethereum_test.go └── solana │ └── solana.go ├── LICENSE ├── headless ├── headless_test.go ├── client.go ├── headless.go └── launcher.go ├── flake.lock ├── cli ├── generate │ └── upload.go └── query │ └── query.go ├── flake.nix └── Makefile /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /config/cli.sample.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | hostname = "http://localhost:9800" 3 | query_path = "/v1/proof" 4 | upload_path = "/v1/proof" 5 | generate_path = "/v1/proof/payload" 6 | 7 | 8 | -------------------------------------------------------------------------------- /types/action.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Action string 4 | 5 | var Actions = struct { 6 | Create Action 7 | Delete Action 8 | }{ 9 | Create: "create", 10 | Delete: "delete", 11 | } 12 | -------------------------------------------------------------------------------- /controller/subkey_payload_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import "testing" 4 | 5 | func Test_SubkeyPayloadSerializeRequest(t *testing.T) { 6 | t.Run("success", func(t *testing.T) { 7 | 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Makefile param 2 | /my.mk 3 | 4 | # Build artifact 5 | build/ 6 | 7 | config/*.json 8 | !config/config.sample.json 9 | 10 | config/*.toml 11 | !config/cli.sample.toml 12 | 13 | .idea 14 | .vscode/ 15 | -------------------------------------------------------------------------------- /types/subkey_algorithm.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type SubkeyAlgorithm string 4 | 5 | var SubkeyAlgorithms = struct { 6 | Secp256R1 SubkeyAlgorithm 7 | Secp256K1 SubkeyAlgorithm 8 | }{ 9 | Secp256R1: "es256", 10 | Secp256K1: "secp256k1", 11 | } 12 | -------------------------------------------------------------------------------- /controller/proof_reupload.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // proofReupload revalidate and save a proof which was sent as a 6 | // proof post but haven't been been called in `POST /v1/proof`. 7 | func proofReupload(c *gin.Context) { 8 | c.JSON(200, gin.H{"TODO": "implement me"}) 9 | } 10 | -------------------------------------------------------------------------------- /common/main.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type Runtime string 4 | 5 | var Runtimes = struct { 6 | Standalone Runtime 7 | Lambda Runtime 8 | }{ 9 | Standalone: "standalone", 10 | Lambda: "lambda", 11 | } 12 | 13 | var ( 14 | CurrentRuntime = Runtimes.Standalone 15 | Environment = "unknown" 16 | Revision = "UNKNOWN" 17 | BuildTime = "0" 18 | ) 19 | -------------------------------------------------------------------------------- /util/base1024/base1024_emoji_alphabet.go: -------------------------------------------------------------------------------- 1 | package base1024 2 | 3 | const Base1024EmojiAlphabet = "0,2pz8,1,5n,2,4h,4,d,6,f,7,3,h,2v,j,o,k,l,l,3,u,m,w,4v,1u,3,4y,3,50,2,53,3,7e,3,7h,2,es,2,gj,c,gp,2,hd,8,hf,3,hg,2,hm,d,hn,3,hr,3,hs,5,hu,e,hw,3,hx,9,hz,a,i0,6,i3,d,i6,9,i9,3,ia,2,ib,5,ic,7,id,4,ie,7,ks,1d,mq,6,my,3,mz,b,n5,4,n6,2,n8,4,n9,3,nh,6e,nt,82,p3,2,pd,2,qk,2,qo,4,rt,3,rz,4" 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.go] 11 | indent_style = tab 12 | indent_size = 8 13 | 14 | [go.mod] 15 | indent_style = tab 16 | indent_size = 8 17 | 18 | [go.sum] 19 | indent_style = tab 20 | indent_size = 8 21 | 22 | [Makefile] 23 | indent_style = tab 24 | indent_size = 8 25 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | postgres: 4 | image: postgres:13-alpine 5 | ports: 6 | - 127.0.0.1:5433:5432 7 | environment: 8 | POSTGRES_USER: proof_server 9 | POSTGRES_PASSWORD: iehohp6iep5eez5fai3eechohdieQuee 10 | POSTGRES_DB: proof_server_dev 11 | volumes: 12 | - proof_server_db:/var/lib/postgresql/data 13 | 14 | volumes: 15 | proof_server_db: {} 16 | -------------------------------------------------------------------------------- /cmd/lambda_headless/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/akrylysov/algnhsa" 5 | "github.com/nextdotid/proof_server/common" 6 | "github.com/nextdotid/proof_server/headless" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | logrus.SetLevel(logrus.InfoLevel) 12 | common.CurrentRuntime = common.Runtimes.Lambda 13 | headless.Init("/opt/chromium", "") 14 | } 15 | 16 | func main() { 17 | algnhsa.ListenAndServe(headless.Engine, nil) 18 | } 19 | -------------------------------------------------------------------------------- /model/main_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/nextdotid/proof_server/config" 8 | ) 9 | 10 | func before_each(t *testing.T) { 11 | // Clean DB 12 | DB.Where("1 = 1").Delete(&Proof{}) 13 | DB.Where("1 = 1").Delete(&ProofChain{}) 14 | DB.Where("1 = 1").Delete(&AvatarAlias{}) 15 | } 16 | 17 | func TestMain(m *testing.M) { 18 | config.Init("../config/config.test.json") 19 | Init(true) 20 | before_each(nil) 21 | 22 | os.Exit(m.Run()) 23 | } 24 | -------------------------------------------------------------------------------- /types/mq.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type QueueAction string 4 | 5 | var QueueActions = struct { 6 | Revalidate QueueAction 7 | ArweaveUpload QueueAction 8 | }{ 9 | Revalidate: "revalidate", 10 | ArweaveUpload: "arweave_upload", 11 | } 12 | 13 | // QueueMessage indicates structure of messages in Amazon SQS. 14 | type QueueMessage struct { 15 | Action QueueAction `json:"action"` 16 | // For revalidate. 17 | ProofID int64 `json:"proof_id"` 18 | // For Arweave upload. 19 | Persona string `json:"persona"` 20 | } 21 | -------------------------------------------------------------------------------- /util/base1024/encode_test.go: -------------------------------------------------------------------------------- 1 | package base1024 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestEncodeToString(t *testing.T) { 10 | t.Run("success", func(t *testing.T) { 11 | res := EncodeToString([]byte("Maskbook")) 12 | fmt.Println(res) 13 | assert.Equal(t, "🐟🔂🏁🤖💧🚊😤", res) 14 | 15 | }) 16 | 17 | t.Run("fail", func(t *testing.T) { 18 | res := EncodeToString([]byte("MaskBook")) 19 | assert.NotEqual(t, "🐟🔂🏁🤖💧🚊😤", res) 20 | 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /docs/scenario-on-chain-game.org: -------------------------------------------------------------------------------- 1 | * Full on-chain game 2 | :PROPERTIES: 3 | :ID: 7bc04e38-459b-4e06-a466-431d5923f9e2 4 | :END: 5 | 6 | ** Preseq 7 | :PROPERTIES: 8 | :ID: f97801a4-29fa-47e9-a53f-3cf42df5e628 9 | :END: 10 | 11 | *** Cross-Device identities 12 | :PROPERTIES: 13 | :ID: a659b334-b3d5-4394-8400-8eaab0bec1e2 14 | :END: 15 | 16 | Merge all separate device key into 1 individual to sync game progress. 17 | 18 | **** Potential solution: Subkey 19 | :PROPERTIES: 20 | :ID: a1614d5d-f015-418a-8de6-51001f99678e 21 | :END: 22 | -------------------------------------------------------------------------------- /test_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=production' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" -o headless_amd64 ./cmd/headless 4 | docker build -t ghcr.io/nextdotid/proof_service_headless:latest --build-arg TARGETARCH=amd64 -f .github/workflows/docker/Dockerfile.headless . 5 | # docker run --rm -it -p 9801:9801 ghcr.io/nextdotid/proof_service_headless:latest 6 | docker save ghcr.io/nextdotid/proof_service_headless:latest | ssh oracle_primary docker load 7 | -------------------------------------------------------------------------------- /.github/workflows/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # WARNING: For github workflow only. 3 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 4 | FROM docker.io/ubuntu:22.04 5 | LABEL org.opencontainers.image.authors="Nyk Ma " 6 | WORKDIR /app 7 | RUN mkdir /app/config && chown -R 1000:1000 /app 8 | 9 | USER 1000:1000 10 | VOLUME [ "/app/config" ] 11 | EXPOSE 9800 12 | CMD [ "proof_server", "-config", "/app/config/config.json", "-port", "9800" ] 13 | 14 | ARG TARGETARCH 15 | COPY ./server_${TARGETARCH} /usr/local/bin/proof_server 16 | -------------------------------------------------------------------------------- /.github/workflows/docker/Dockerfile.headless: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # WARNING: For github workflow only. 3 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 4 | # Base image: https://github.com/Zenika/alpine-chrome 5 | FROM gcr.io/zenika-hub/alpine-chrome:100 6 | LABEL org.opencontainers.image.authors="Nyk Ma " 7 | 8 | EXPOSE 9801 9 | ENTRYPOINT ["/bin/ash", "-c"] 10 | CMD ["/usr/local/bin/ps_headless", "-chromium", "/usr/bin/chromium-browser", "-port", "9801"] 11 | 12 | ARG TARGETARCH 13 | COPY ./headless_${TARGETARCH} /usr/local/bin/ps_headless 14 | -------------------------------------------------------------------------------- /config/config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "host": "localhost", 4 | "read_only_hosts": ["localhost"], 5 | "port": 5433, 6 | "user": "proof_server", 7 | "password": "iehohp6iep5eez5fai3eechohdieQuee", 8 | "db_name": "proof_server_dev", 9 | "tz": "UTC" 10 | }, 11 | "platform": { 12 | "twitter": { 13 | "access_token": "xxxx", 14 | "access_token_secret": "xxxx", 15 | "consumer_key": "xxxx", 16 | "consumer_secret": "xxxx" 17 | }, 18 | "ethereum": { 19 | "rpc_server": "https://polygon-rpc.com/" 20 | }, 21 | "discord": { 22 | "bot_token": "", 23 | "proof_server_channel_id": "" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /validator/twitter/api_test.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_fetchPostWithAPI(t *testing.T) { 10 | t.Run("success", func(t *testing.T) { 11 | tweet, err := fetchPostWithAPI("1652176440396517378", 10) 12 | require.NoError(t, err) 13 | require.Contains(t, tweet.Text, "Sig:") 14 | require.Equal(t, tweet.User.ScreenName, "bgm38") 15 | require.Equal(t, tweet.User.ID, "292254624") 16 | }) 17 | } 18 | 19 | func Test_fetchUserName(t *testing.T) { 20 | t.Run("success", func(t *testing.T) { 21 | userName, err := fetchUserName("292254624") 22 | require.NoError(t, err) 23 | require.Equal(t, "bgm38", userName) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/base64" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/nextdotid/proof_server/util/base1024" 9 | 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | func TimeToTimestampString(now time.Time) string { 14 | return strconv.FormatInt(now.Unix(), 10) 15 | } 16 | 17 | func TimestampStringToTime(now string) (time.Time, error) { 18 | ts, err := strconv.ParseInt(now, 10, 64) 19 | if err != nil { 20 | return time.Time{}, xerrors.Errorf("%w", err) 21 | } 22 | 23 | return time.Unix(ts, int64(0)), nil 24 | } 25 | 26 | func DecodeString(s string) ([]byte, error) { 27 | sigBytes, err := base64.StdEncoding.DecodeString(s) 28 | if err == nil { 29 | return sigBytes, nil 30 | } 31 | return base1024.DecodeString(s) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/headless/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/nextdotid/proof_server/common" 8 | "github.com/nextdotid/proof_server/headless" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | flagPort = flag.Int("port", 9801, "Listen port") 14 | flagChromiumPath = flag.String("chromium", "/usr/bin/chromium-browser", "Path to Chromium executable") 15 | flagReplace = flag.String("replace", "", "URL Replacement rule (orig=new,orig2=new2)") 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | logrus.SetLevel(logrus.DebugLevel) 21 | common.CurrentRuntime = common.Runtimes.Standalone 22 | headless.Init(*flagChromiumPath, *flagReplace) 23 | 24 | listen := fmt.Sprintf("0.0.0.0:%d", *flagPort) 25 | headless.Engine.Run(listen) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/nextdotid/proof_server/cli/generate" 10 | "github.com/nextdotid/proof_server/cli/query" 11 | ) 12 | 13 | const ( 14 | OPERATION_QUERY = 1 15 | OPERATION_GENERATE = 2 16 | ) 17 | 18 | func main() { 19 | input := bufio.NewScanner(os.Stdin) 20 | fmt.Println("Choose the process\n 1. query the exists proof\n 2. generate the signature and upload to proof service\nEnter the number of above process") 21 | 22 | input.Scan() 23 | operation, _ := strconv.Atoi(input.Text()) 24 | 25 | switch operation { 26 | case OPERATION_QUERY: 27 | query.QueryProof() 28 | case OPERATION_GENERATE: 29 | generate.GeneratePayload() 30 | default: 31 | fmt.Printf("Unknow Operation: %s", operation) 32 | os.Exit(-1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/playground2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/nextdotid/proof_server/util/crypto" 8 | ) 9 | 10 | func main() { 11 | payload := "{\"action\":\"create\",\"created_at\":\"1705413052\",\"identity\":\"johndic94329223\",\"platform\":\"twitter\",\"prev\":null,\"uuid\":\"6f1fa69c-13f8-4bec-acf6-c24003633df8\"}" 12 | signatureBase64 := "VUwqMOFtkGGu0RqIy2HhoOyWZIdNEd29IP5ESeaWIMcZjAyCn3t1/0CmN5WaISTi1RFUOVCSw9WKC3mh78YKihw=" 13 | signature, err := base64.StdEncoding.DecodeString(signatureBase64) 14 | if err != nil { 15 | panic(err) 16 | } 17 | pubkey, err := crypto.RecoverPubkeyFromPersonalSignature(payload, signature) 18 | if err != nil { 19 | panic(err) 20 | } 21 | fmt.Printf("Public key: 0x%s\n", crypto.CompressedPubkeyHex(pubkey)) 22 | fmt.Println("Success") 23 | } 24 | -------------------------------------------------------------------------------- /util/base1024/decode_test.go: -------------------------------------------------------------------------------- 1 | package base1024 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDecodeString(t *testing.T) { 9 | t.Run("Equally", func(t *testing.T) { 10 | str := "🐟🔂🏁🤖💧🚊😤" 11 | res, err := DecodeString(str) 12 | assert.Nil(t, err) 13 | assert.Equal(t, "Maskbook", string(res)) 14 | }) 15 | 16 | t.Run("Not Equal", func(t *testing.T) { 17 | str := "🐟🔂🏁🤖💧" 18 | res, err := DecodeString(str) 19 | assert.Nil(t, err) 20 | assert.NotEqual(t, "Maskbook", string(res)) 21 | }) 22 | 23 | t.Run("Decode Playload", func(t *testing.T) { 24 | str := "👲🍚🍾🔆🏠🚱👧🦢🕟🛷🔭💘😝🙂🚳🦜🔙🔊🚗🏏👪🛹🗣🏳🦐🥫🦺🚎🕗🚷💡🚁🎟🗯📰🐊🕳🥠💐🎛🤵🆘🔣📥🦝🔉🌊🥠🥅🍏🥜🃏" 25 | res, err := DecodeString(str) 26 | assert.Nil(t, err) 27 | assert.NotEqual(t, "Maskbook", string(res)) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /util/base1024/init.go: -------------------------------------------------------------------------------- 1 | package base1024 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | "html" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const TAIL = "\\ud83c\\udfad" 11 | 12 | var Emojis []string 13 | 14 | func init() { 15 | base1024EmojiAlphabetList := strings.Split(Base1024EmojiAlphabet, ",") 16 | values := make([]int64, 0) 17 | values = lo.Map(base1024EmojiAlphabetList, func(x string, _ int) int64 { 18 | tmp, _ := strconv.ParseInt(x, 36, 64) 19 | return tmp 20 | }) 21 | 22 | points := make([]int64, 1024) 23 | for i := range points { 24 | points[i] = 1 25 | } 26 | 27 | for i := 0; i < len(values); i += 2 { 28 | // [index, value, index, value, ...] 29 | points[values[i]] = values[i+1] 30 | } 31 | for i := 1; i < len(points); i += 1 { 32 | points[i] = points[i-1] + points[i] 33 | } 34 | 35 | Emojis = lo.Map(points, func(x int64, _ int) string { 36 | return html.UnescapeString(string(rune(x))) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /controller/proof_exists_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_proofExists(t *testing.T) { 11 | t.Run("smoke", func(t *testing.T) { 12 | before_each(t) 13 | 14 | resp := ErrorResponse{} 15 | APITestCall( 16 | Engine, 17 | "GET", 18 | fmt.Sprintf("/v1/proof/exists?platform=twitter&identity=test&public_key=%s", persona), 19 | nil, 20 | &resp, 21 | ) 22 | assert.Contains(t, resp.Message, "not found") 23 | }) 24 | 25 | t.Run("success", func(t *testing.T) { 26 | before_each(t) 27 | insert_proof(t) 28 | resp_body := ProofExistsResponse{} 29 | resp := APITestCall( 30 | Engine, 31 | "GET", 32 | fmt.Sprintf("/v1/proof/exists?platform=twitter&identity=yeiwb&public_key=%s", persona), 33 | nil, 34 | &resp_body, 35 | ) 36 | assert.Equal(t, 200, resp.Code) 37 | t.Logf("%s", resp.Body.String()) 38 | assert.Equal(t, true, resp_body.IsValid) 39 | assert.Equal(t, "", resp_body.InvalidReason) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /types/platform.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Platform string 4 | 5 | // Platforms is a list of all current supported platforms, DO NOT MODIFY IT IN RUNTIME. 6 | var Platforms = struct { 7 | Github Platform 8 | NextID Platform 9 | Twitter Platform 10 | Telegram Platform 11 | TikTok Platform 12 | Keybase Platform 13 | Ethereum Platform 14 | Discord Platform 15 | Das Platform 16 | Solana Platform 17 | Minds Platform 18 | DNS Platform 19 | ENS Platform 20 | Steam Platform 21 | ActivityPub Platform 22 | Slack Platform 23 | }{ 24 | Github: "github", 25 | NextID: "nextid", 26 | Twitter: "twitter", 27 | Telegram: "telegram", 28 | TikTok: "tiktok", 29 | Keybase: "keybase", 30 | Ethereum: "ethereum", 31 | Discord: "discord", 32 | Das: "dotbit", 33 | Solana: "solana", 34 | Minds: "minds", 35 | DNS: "dns", 36 | ENS: "ens", 37 | Steam: "steam", 38 | ActivityPub: "activitypub", 39 | Slack: "slack", 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/PR.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request Check 2 | on: 3 | pull_request: 4 | branches: [develop, master] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go: ['1.19'] 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v3 15 | - name: Setup Go v1.x 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: ${{ matrix.go }} 19 | cache: true 20 | - name: Check format 21 | run: if [ -z $(gofmt -l .) ]; then echo 'Format check passed.'; else echo 'Format check failed. Please run gofmt by yourself before committing.'; exit 1; fi 22 | - name: Build 23 | env: 24 | CGO_ENABLED: '0' 25 | GOARCH: amd64 26 | GOOS: linux 27 | run: go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=development' -X 'github.com/nextdotid/proof_server/common.Revision=${{ github.sha }}' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" ./... 28 | # - name: Test 29 | # run: go test -v ./... 30 | -------------------------------------------------------------------------------- /model/avatar_alias_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/nextdotid/proof_server/util/crypto" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func GenerateAliasTo(avatarPubkey any) string { 12 | pc1 := GenerateProofChain() 13 | 14 | avatar := AvatarAlias{ 15 | CreatedAt: time.Now(), 16 | Avatar: MarshalAvatar(avatarPubkey), 17 | Alias: MarshalAvatar(pc1.Persona), 18 | ProofChainID: pc1.ID, 19 | } 20 | tx := DB.Create(&avatar) 21 | if tx.Error != nil { 22 | panic(tx.Error) 23 | } 24 | return avatar.Alias 25 | } 26 | 27 | func Test_FindAllAliasByAvatar(t *testing.T) { 28 | t.Run("success", func(t *testing.T) { 29 | avatar, _ := crypto.GenerateSecp256k1Keypair() 30 | alias1 := GenerateAliasTo(avatar) 31 | alias2 := GenerateAliasTo(alias1) 32 | alias3 := GenerateAliasTo(alias1) 33 | 34 | aliases, err := FindAllAliasByAvatar(MarshalAvatar(avatar)) 35 | require.NoError(t, err) 36 | require.Contains(t, aliases, alias1) 37 | require.Contains(t, aliases, alias2) 38 | require.Contains(t, aliases, alias3) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /controller/subkey_payload.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nextdotid/proof_server/model" 6 | "github.com/nextdotid/proof_server/types" 7 | ) 8 | 9 | type subkeyPayloadRequest struct { 10 | Avatar string `json:"avatar"` 11 | Algorithm types.SubkeyAlgorithm `json:"algorithm"` 12 | PublicKey string `json:"public_key"` 13 | RP_ID string `json:"rp_id"` 14 | } 15 | 16 | type subkeyPayloadResponse struct { 17 | SignPayload string `json:"sign_payload"` 18 | } 19 | 20 | // POST /v1/subkey/payload 21 | func subkeyPayload(c *gin.Context) { 22 | req := subkeyPayloadRequest{} 23 | if err := c.BindJSON(&req); err != nil { 24 | errorResp(c, 400, err) 25 | return 26 | } 27 | 28 | subkey := model.Subkey{ 29 | Algorithm: req.Algorithm, 30 | Avatar: req.Avatar, 31 | PublicKey: req.PublicKey, 32 | RP_ID: req.RP_ID, 33 | } 34 | payload, err := subkey.SignPayload() 35 | if err != nil { 36 | errorResp(c, 400, err) 37 | return 38 | } 39 | c.JSON(200, subkeyPayloadResponse{ 40 | SignPayload: payload, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dimension 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /headless/headless_test.go: -------------------------------------------------------------------------------- 1 | package headless_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/nextdotid/proof_server/config" 13 | "github.com/nextdotid/proof_server/headless" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestMain(m *testing.M) { 18 | config.Init("../config/config.test.json") 19 | headless.Init("", "") 20 | os.Exit(m.Run()) 21 | } 22 | 23 | func APITestCall(engine *gin.Engine, method, url string, body any, response any) *httptest.ResponseRecorder { 24 | bb, _ := json.Marshal(body) 25 | w := httptest.NewRecorder() 26 | req, _ := http.NewRequest(method, url, bytes.NewReader(bb)) 27 | req.Header.Add("Content-Type", "application/json") 28 | engine.ServeHTTP(w, req) 29 | json.Unmarshal(w.Body.Bytes(), response) 30 | 31 | return w 32 | } 33 | 34 | func Test_initUrlReplacementRule(t *testing.T) { 35 | t.Run("success", func(t *testing.T) { 36 | flagReplacement := "abc.com=test.org,def.com=foobar.net" 37 | headless.InitUrlReplacementRule(flagReplacement) 38 | require.Equal(t, "test.org", headless.URLReplacement["abc.com"]) 39 | require.Equal(t, "foobar.net", headless.URLReplacement["def.com"]) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /controller/main_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/nextdotid/proof_server/validator/discord" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/nextdotid/proof_server/config" 15 | "github.com/nextdotid/proof_server/model" 16 | "github.com/nextdotid/proof_server/validator/ethereum" 17 | "github.com/nextdotid/proof_server/validator/github" 18 | "github.com/nextdotid/proof_server/validator/keybase" 19 | "github.com/nextdotid/proof_server/validator/twitter" 20 | ) 21 | 22 | func before_each(t *testing.T) { 23 | // Clean DB 24 | model.DB.Where("1 = 1").Delete(&model.Proof{}) 25 | model.DB.Where("1 = 1").Delete(&model.ProofChain{}) 26 | } 27 | 28 | func TestMain(m *testing.M) { 29 | config.Init("../config/config.test.json") 30 | model.Init(true) 31 | Init() 32 | 33 | twitter.Init() 34 | keybase.Init() 35 | ethereum.Init() 36 | github.Init() 37 | discord.Init() 38 | 39 | before_each(nil) 40 | 41 | os.Exit(m.Run()) 42 | } 43 | 44 | func APITestCall(engine *gin.Engine, method, url string, body any, response any) *httptest.ResponseRecorder { 45 | body_bytes, _ := json.Marshal(body) 46 | w := httptest.NewRecorder() 47 | req, _ := http.NewRequest(method, url, bytes.NewReader(body_bytes)) 48 | engine.ServeHTTP(w, req) 49 | 50 | json.Unmarshal(w.Body.Bytes(), response) 51 | return w 52 | } 53 | -------------------------------------------------------------------------------- /model/main.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | "github.com/sirupsen/logrus" 6 | "gorm.io/driver/postgres" 7 | "gorm.io/gorm" 8 | 9 | "github.com/nextdotid/proof_server/config" 10 | ) 11 | 12 | var ( 13 | DB *gorm.DB 14 | // Since this service is mostly run as a lambda, we don't need 15 | // to init an array here. When lambda scaled to a very large 16 | // number, servers in `read_only_hosts` will be used evenly. 17 | ReadOnlyDB *gorm.DB 18 | l = logrus.WithFields(logrus.Fields{"module": "model"}) 19 | ) 20 | 21 | // Init initializes DB connection instance and do migration at startup. 22 | func Init(autoMigrate bool) { 23 | if DB != nil { // initialized 24 | return 25 | } 26 | dsn := config.GetDatabaseDSN(config.C.DB.Host) 27 | var err error 28 | 29 | DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) 30 | if err != nil { 31 | l.Fatalf("Error when opening DB: %s\n", err.Error()) 32 | } 33 | 34 | if autoMigrate { 35 | err = DB.AutoMigrate( 36 | &Proof{}, 37 | &ProofChain{}, 38 | &AvatarAlias{}, 39 | &Subkey{}, 40 | ) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | readOnlyHost := lo.Sample(config.C.DB.ReadOnlyHosts) 47 | readOnlyDSN := config.GetDatabaseDSN(readOnlyHost) 48 | ReadOnlyDB, err = gorm.Open(postgres.Open(readOnlyDSN), &gorm.Config{}) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | l.Info("database initialized") 54 | } 55 | -------------------------------------------------------------------------------- /validator/tiktok/fetcher_test.go: -------------------------------------------------------------------------------- 1 | package tiktok 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_finalURLmatching(t *testing.T) { 10 | t.Run("success", func(t *testing.T) { 11 | url := "https://www.tiktok.com/@scout_2015/video/6718335390845095173?_test=123" 12 | result := finalUrlRegexp.FindStringSubmatch(url) 13 | require.Equal(t, 3, len(result)) 14 | require.Equal(t, "scout_2015", result[1]) 15 | require.Equal(t, "6718335390845095173", result[2]) 16 | }) 17 | } 18 | 19 | func Test_redirectToFinalURL(t *testing.T) { 20 | t.Run("success", func(t *testing.T) { 21 | url := "https://www.tiktok.com/t/ZPRv3FPg5/" 22 | username, videoID, err := redirectToFinalURL(url, 0) 23 | require.NoError(t, err) 24 | require.Equal(t, "realwolfiesmom", username) 25 | require.Equal(t, "7287329983805197614", videoID) 26 | }) 27 | } 28 | 29 | func Test_fetchOembedInfo(t *testing.T) { 30 | t.Run("full URL", func(t *testing.T) { 31 | url := "https://www.tiktok.com/@scout2015/video/6718335390845095173" 32 | result, err := fetchOembedInfo(url) 33 | require.NoError(t, err) 34 | require.Contains(t, result.Title, "Scramble up ur name") 35 | }) 36 | 37 | t.Run("shortened URL", func(t *testing.T) { 38 | url := "https://www.tiktok.com/t/ZPRv3FPg5" 39 | result, err := fetchOembedInfo(url) 40 | require.NoError(t, err) 41 | require.Contains(t, result.EmbedProductID, "7287329983805197614") 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /validator/activitypub/mastodon.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "golang.org/x/xerrors" 9 | ) 10 | 11 | const MASTODON_API_STATUS = "https://%s/api/v1/statuses/%s" 12 | 13 | type MastodonResponse struct { 14 | Account MastodonResponseAccount `json:"account"` 15 | Content string `json:"content"` 16 | } 17 | 18 | type MastodonResponseAccount struct { 19 | // ASCII 20 | Username string `json:"username"` 21 | // Digits 22 | Id string `json:"id"` 23 | } 24 | 25 | // GetMastodonText can also deal with Pleroma server. 26 | func (ap *ActivityPub) GetMastodonText() (err error) { 27 | _, server, err := ap.SplitID() 28 | if err != nil { 29 | return err 30 | } 31 | resp, err := http.Get(fmt.Sprintf(MASTODON_API_STATUS, server, ap.ProofLocation)) 32 | if err != nil { 33 | return xerrors.Errorf("failed to get mastodon / pleroma status: %w", err) 34 | } 35 | var response MastodonResponse 36 | err = json.NewDecoder(resp.Body).Decode(&response) 37 | if err != nil { 38 | return xerrors.Errorf("failed to decode mastodon / pleroma status: %w", err) 39 | } 40 | 41 | postIdentity := fmt.Sprintf("@%s@%s", response.Account.Username, server) 42 | if postIdentity != ap.Identity { 43 | return xerrors.Errorf("failed to identify mastodon / pleroma status: identity mismatch: %s != %s", postIdentity, ap.Identity) 44 | } 45 | 46 | ap.AltID = response.Account.Id 47 | ap.Text = response.Content 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /validator/steam/test_76561198092541763.xml: -------------------------------------------------------------------------------- 1 | 2 | 76561198092541763 3 | 4 | online 5 | 6 | public 7 | 3 8 | 9 | 10 | 11 | 1 12 | None 13 | 0 14 | 15 | May 26, 2013 16 | 17 | 0.0 18 | 19 | 20 | 21 | https://steamcommunity.com/tradeoffer/new/?partner=132276035&token=elkrXoqe ]]> 22 | 23 | -------------------------------------------------------------------------------- /util/base1024/encode.go: -------------------------------------------------------------------------------- 1 | package base1024 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | "strings" 6 | ) 7 | 8 | func EncodeToString(input []byte) string { 9 | inputLength := len(input) 10 | 11 | remainder := inputLength % 5 12 | safe := inputLength - remainder 13 | points := make([]int, 0) 14 | 15 | for i := 0; i <= safe; i += 5 { 16 | tmp := make([]int, 0) 17 | if i+1 < inputLength { 18 | tmp = append(tmp, (int(input[i])&0xff)<<2|(int(input[i+1])>>6)) 19 | } else if i+1 == inputLength { 20 | tmp = append(tmp, (int(input[i])&0xff)<<2) 21 | } 22 | 23 | if i+2 < inputLength { 24 | tmp = append(tmp, (int(input[i+1])&0x3f)<<4|(int(input[i+2])>>4)) 25 | } else if i+2 == inputLength { 26 | tmp = append(tmp, (int(input[i+1])&0x3f)<<4) 27 | } 28 | 29 | if i+3 < inputLength { 30 | tmp = append(tmp, (int(input[i+2])&0xf)<<6|(int(input[i+3])>>2)) 31 | } else if i+3 == inputLength { 32 | tmp = append(tmp, (int(input[i+2])&0xf)<<6) 33 | } 34 | 35 | if i+4 < inputLength { 36 | tmp = append(tmp, (int(input[i+3])&0x3)<<8|int(input[i+4])) 37 | } else if i+4 == inputLength { 38 | tmp = append(tmp, (int(input[i+3])&0x3)<<8) 39 | } 40 | 41 | if i < safe { 42 | points = append(points, tmp...) 43 | } else if i >= safe && remainder != 0 { 44 | points = append(points, tmp[0:inputLength-safe]...) 45 | } 46 | } 47 | 48 | resList := lo.Map(points, func(x int, _ int) string { 49 | return Emojis[x] 50 | }) 51 | resStr := strings.Join(resList, "") 52 | if remainder == 4 { 53 | resStr += TAIL 54 | } 55 | return resStr 56 | } 57 | -------------------------------------------------------------------------------- /controller/proof_chain_single.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/nextdotid/proof_server/model" 9 | "github.com/samber/lo" 10 | ) 11 | 12 | type ProofChainSingleRequest struct { 13 | LastID int `form:"last_id"` 14 | Count int `form:"count"` 15 | } 16 | 17 | type ProofChainSingleResponse struct { 18 | Links []ProofChainSingleItem `json:"links"` 19 | } 20 | 21 | type ProofChainSingleItem struct { 22 | model.ProofChainItem 23 | Avatar string `json:"avatar"` 24 | ID int64 `json:"id"` 25 | } 26 | 27 | // proofChainChanges returns proof chain one by one, unlinked. 28 | func proofChainChanges(c *gin.Context) { 29 | req := ProofChainSingleRequest{} 30 | if err := c.BindQuery(&req); err != nil { 31 | errorResp(c, http.StatusBadRequest, errors.New("Param error")) 32 | return 33 | } 34 | if req.Count <= 0 { 35 | req.Count = 10 36 | } 37 | if req.Count >= 100 { 38 | req.Count = 100 39 | } 40 | 41 | pc_found := make([]model.ProofChain, 0, 0) 42 | tx := model.ReadOnlyDB.Where("id > ?", req.LastID).Limit(req.Count).Order("id ASC").Find(&pc_found) 43 | if tx.Error != nil { 44 | errorResp(c, http.StatusInternalServerError, tx.Error) 45 | return 46 | } 47 | 48 | proof_chains := lo.Map(pc_found, func(pc model.ProofChain, i int) ProofChainSingleItem { 49 | return ProofChainSingleItem{ 50 | ProofChainItem: pc.ToProofChainItem(), 51 | Avatar: pc.Persona, 52 | ID: pc.ID, 53 | } 54 | }) 55 | 56 | c.JSON(http.StatusOK, ProofChainSingleResponse{ 57 | Links: proof_chains, 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /model/subkey_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "testing" 6 | 7 | "github.com/nextdotid/proof_server/types" 8 | "github.com/nextdotid/proof_server/util/crypto" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func generateK1Subkey() (subkey *Subkey, avatarSK *ecdsa.PrivateKey, subkeySK *ecdsa.PrivateKey) { 13 | avatarPK, avatarSK := crypto.GenerateSecp256k1Keypair() 14 | subkeyPK, subkeySK := crypto.GenerateSecp256k1Keypair() 15 | avatarPKHex := "0x" + crypto.CompressedPubkeyHex(avatarPK) 16 | subkeyPKHex := "0x" + crypto.CompressedPubkeyHex(subkeyPK) 17 | return &Subkey{ 18 | Name: "Yubikey", 19 | RP_ID: "apple.com", 20 | Avatar: avatarPKHex, 21 | Algorithm: types.SubkeyAlgorithms.Secp256K1, 22 | PublicKey: subkeyPKHex, 23 | }, avatarSK, subkeySK 24 | } 25 | 26 | func Test_SignPayload(t *testing.T) { 27 | t.Run("success", func(t *testing.T) { 28 | subkey, _, _ := generateK1Subkey() 29 | payload, err := subkey.SignPayload() 30 | require.NoError(t, err) 31 | require.Contains(t, payload, subkey.Avatar) 32 | require.Contains(t, payload, subkey.Algorithm) 33 | }) 34 | } 35 | 36 | func Test_ValidateSignature(t *testing.T) { 37 | t.Run("success", func(t *testing.T) { 38 | subkey, avatarSK, _ := generateK1Subkey() 39 | payload, _ := subkey.SignPayload() 40 | signature, err := crypto.SignPersonal([]byte(payload), avatarSK) 41 | require.NoError(t, err) 42 | // require.NoError(t, crypto.ValidatePersonalSignature(payload, signature, &avatarSK.PublicKey)) 43 | require.NoError(t, subkey.ValidateSignature(payload, signature)) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /util/sqs/sqs_test.go: -------------------------------------------------------------------------------- 1 | package sqs 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/sqs" 9 | "github.com/stretchr/testify/assert" 10 | 11 | myconfig "github.com/nextdotid/proof_server/config" 12 | ) 13 | 14 | type SQSSendMessageImpl struct{} 15 | 16 | func (dt SQSSendMessageImpl) GetQueueUrl(ctx context.Context, 17 | params *sqs.GetQueueUrlInput, 18 | optFns ...func(*sqs.Options)) (*sqs.GetQueueUrlOutput, error) { 19 | 20 | prefix := "https://sqs.REGION.amazonaws.com/ACCOUNT#/" 21 | 22 | output := &sqs.GetQueueUrlOutput{ 23 | QueueUrl: aws.String(prefix + "aws-docs-example-queue-url1"), 24 | } 25 | 26 | return output, nil 27 | } 28 | 29 | func (dt SQSSendMessageImpl) SendMessage(ctx context.Context, 30 | params *sqs.SendMessageInput, 31 | optFns ...func(*sqs.Options)) (*sqs.SendMessageOutput, error) { 32 | 33 | output := &sqs.SendMessageOutput{ 34 | MessageId: aws.String("aws-docs-example-messageID"), 35 | } 36 | 37 | return output, nil 38 | } 39 | 40 | func before_each(t *testing.T) { 41 | myconfig.C = &myconfig.Config{} 42 | myconfig.C.Sqs.QueueName = "test" 43 | 44 | api = &SQSSendMessageImpl{} 45 | 46 | gqInput := &sqs.GetQueueUrlInput{QueueName: aws.String(myconfig.C.Sqs.QueueName)} 47 | result, err := getQueueUrl(context.Background(), api, gqInput) 48 | if err != nil { 49 | t.Fatalf("error getting queue url: %v", err) 50 | } 51 | 52 | queueUrl = result.QueueUrl 53 | } 54 | 55 | func Test_Send(t *testing.T) { 56 | t.Run("success", func(t *testing.T) { 57 | before_each(t) 58 | assert.NoError(t, Send(struct{ x string }{x: "y"})) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /controller/proof_upload_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nextdotid/proof_server/model" 7 | "github.com/nextdotid/proof_server/types" 8 | "github.com/nextdotid/proof_server/util" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_ProofUpload(t *testing.T) { 13 | t.Run("success", func(t *testing.T) { 14 | before_each(t) 15 | 16 | req := ProofUploadRequest{ 17 | Action: types.Actions.Create, 18 | Platform: types.Platforms.Twitter, 19 | Identity: "yeiwb", 20 | ProofLocation: "1504363098328924163", 21 | PublicKey: "0x03666b700aeb6a6429f13cbb263e1bc566cd975a118b61bc796204109c1b351d19", 22 | CreatedAt: "1647503071", 23 | Uuid: "c6fa1483-1bad-4f07-b661-678b191ab4b3", 24 | } 25 | resp := ErrorResponse{} 26 | APITestCall(Engine, "POST", "/v1/proof", &req, &resp) 27 | assert.Empty(t, resp.Message) 28 | 29 | pc := model.ProofChain{ 30 | Action: req.Action, 31 | Platform: req.Platform, 32 | Identity: req.Identity, 33 | Location: req.ProofLocation, 34 | } 35 | model.DB.Where(&pc).First(&pc) 36 | assert.Greater(t, pc.ID, int64(0)) 37 | assert.Equal(t, req.PublicKey, pc.Persona) 38 | orig_created_at, _ := util.TimestampStringToTime(req.CreatedAt) 39 | assert.Equal(t, pc.CreatedAt, orig_created_at) 40 | assert.Equal(t, pc.AltID, "1468853291941773312") 41 | 42 | proof := model.Proof{ 43 | Platform: req.Platform, 44 | Identity: req.Identity, 45 | Location: req.ProofLocation, 46 | } 47 | model.DB.Where(&proof).First(&proof) 48 | assert.Greater(t, proof.ID, int64(0)) 49 | assert.Equal(t, req.PublicKey, proof.Persona) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /validator/activitypub/activitypub_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/types" 8 | "github.com/nextdotid/proof_server/util" 9 | "github.com/nextdotid/proof_server/util/crypto" 10 | "github.com/nextdotid/proof_server/validator" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func GenerateMisskeyRecord() (ap *ActivityPub) { 15 | pk, _ := crypto.StringToSecp256k1Pubkey("03c683a83bdf0abae3c344855b55b5978fd22fbedae575bd1f540f919afbc19015") 16 | ca, _ := util.TimestampStringToTime("1671356397") 17 | uuid := uuid.MustParse("4d89b36a-4e55-4c1f-93c8-c81b08f71b09") 18 | 19 | return &ActivityPub{ 20 | Base: &validator.Base{ 21 | Platform: types.Platforms.ActivityPub, 22 | Action: types.Actions.Create, 23 | Pubkey: pk, 24 | Identity: "nykma@t.nyk.app", 25 | ProofLocation: "98wr1tkc82", 26 | CreatedAt: ca, 27 | Uuid: uuid, 28 | }, 29 | } 30 | } 31 | 32 | func Test_ExtractSignature(t *testing.T) { 33 | t.Run("success", func(t *testing.T) { 34 | ap := ActivityPub{ 35 | Base: &validator.Base{ 36 | Text: "Validate my ActivityPub identity @%s for Avatar 0x%s:\n\nSignature: dGVzdDEyMw==\nUUID:%s\nPrevious:%s\nCreatedAt:%d\n\nPowered by Next.ID - Connect All Digital Identities.\n", 37 | }, 38 | } 39 | require.NoError(t, ap.ExtractSignature()) 40 | require.Equal(t, "test123", string(ap.Base.Signature)) 41 | }) 42 | } 43 | 44 | func Test_Validate(t *testing.T) { 45 | t.Run("success", func(t *testing.T) { 46 | ap := GenerateMisskeyRecord() 47 | require.NoError(t, ap.Validate()) 48 | require.Equal(t, ap.AltID, "8zwtspqtym") 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /validator/activitypub/misskey.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | type misskeyNotesShowRequest struct { 13 | NoteID string `json:"noteId"` 14 | } 15 | 16 | // Only focus on `text` field for now. 17 | // TODO: show error message if it is not public. 18 | type misskeyNotesShowResponse struct { 19 | User misskeyNotesShowResponseUser `json:"user"` 20 | Text string `json:"text"` 21 | } 22 | 23 | type misskeyNotesShowResponseUser struct { 24 | Id string `json:"id"` 25 | Username string `json:"username"` 26 | } 27 | 28 | func (ap *ActivityPub) GetMisskeyText() (err error) { 29 | _, server, err := ap.SplitID() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | body := misskeyNotesShowRequest{ 35 | NoteID: ap.ProofLocation, 36 | } 37 | bodyBytes, err := json.Marshal(body) 38 | if err != nil { 39 | return err 40 | } 41 | resp, err := http.Post(fmt.Sprintf("https://%s/api/notes/show", server), "application/json", bytes.NewReader(bodyBytes)) 42 | if err != nil { 43 | return xerrors.Errorf("error when fetching Misskey note: %w", err) 44 | } 45 | var response misskeyNotesShowResponse 46 | err = json.NewDecoder(resp.Body).Decode(&response) 47 | if err != nil { 48 | return xerrors.Errorf("error when decoding Misskey note response: %w", err) 49 | } 50 | postIdentity := fmt.Sprintf("%s@%s", response.User.Username, server) 51 | if postIdentity != ap.Identity { 52 | return xerrors.Errorf("Error when fetching Misskey note: This post is made by %s, not %s", postIdentity, ap.Identity) 53 | } 54 | 55 | ap.AltID = response.User.Id 56 | ap.Text = response.Text 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cmd/playground/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/google/uuid" 8 | "github.com/nextdotid/proof_server/config" 9 | "github.com/nextdotid/proof_server/types" 10 | "github.com/nextdotid/proof_server/util" 11 | "github.com/nextdotid/proof_server/util/crypto" 12 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 13 | "github.com/nextdotid/proof_server/validator" 14 | "github.com/nextdotid/proof_server/validator/twitter" 15 | ) 16 | 17 | func generate() twitter.Twitter { 18 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x02492e9cb3a3578acc27fd1884a6de1758add291300754557d06a28308951d46ea") 19 | created_at, _ := util.TimestampStringToTime("1697953689") 20 | return twitter.Twitter{ 21 | Base: &validator.Base{ 22 | Platform: types.Platforms.Twitter, 23 | Previous: "", 24 | Action: types.Actions.Create, 25 | Pubkey: pubkey, 26 | Identity: "askcasmir", 27 | ProofLocation: "1715976641241919493", 28 | Text: "", 29 | Uuid: uuid.MustParse("64442f89-9cd9-4f62-bbd0-47e5f849f9b4"), 30 | CreatedAt: created_at, 31 | }, 32 | } 33 | } 34 | 35 | func main() { 36 | config.Init("./config/config.json") 37 | 38 | myTwitter := generate() 39 | sigBytes, err := util.DecodeString("Upf+OxdAzaVb0mVxso0PlTDQYf6JjldY/xEo7RkCMM9dM7IgBGgWU5Yk5U5j0RdhmX64Y9GCqziyQD9tHIxxFRs=") 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | myTwitter.SignaturePayload = myTwitter.GenerateSignPayload() 45 | err = crypto.ValidatePersonalSignature(myTwitter.SignaturePayload, sigBytes, myTwitter.Pubkey) 46 | 47 | // err := myTwitter.Validate() 48 | if err != nil { 49 | panic(err) 50 | } 51 | fmt.Println("Validated") 52 | os.Exit(0) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/nextdotid/proof_server/common" 8 | "github.com/nextdotid/proof_server/config" 9 | "github.com/nextdotid/proof_server/controller" 10 | "github.com/nextdotid/proof_server/model" 11 | "github.com/nextdotid/proof_server/validator/activitypub" 12 | "github.com/nextdotid/proof_server/validator/das" 13 | "github.com/nextdotid/proof_server/validator/discord" 14 | "github.com/nextdotid/proof_server/validator/dns" 15 | "github.com/nextdotid/proof_server/validator/ethereum" 16 | "github.com/nextdotid/proof_server/validator/github" 17 | "github.com/nextdotid/proof_server/validator/keybase" 18 | "github.com/nextdotid/proof_server/validator/minds" 19 | "github.com/nextdotid/proof_server/validator/solana" 20 | "github.com/nextdotid/proof_server/validator/steam" 21 | "github.com/nextdotid/proof_server/validator/twitter" 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | var ( 26 | flagConfigPath = flag.String("config", "./config/config.json", "Config.json file path") 27 | flagPort = flag.Int("port", 9800, "Listen port") 28 | ) 29 | 30 | func init_validators() { 31 | twitter.Init() 32 | ethereum.Init() 33 | keybase.Init() 34 | github.Init() 35 | discord.Init() 36 | das.Init() 37 | solana.Init() 38 | minds.Init() 39 | dns.Init() 40 | steam.Init() 41 | activitypub.Init() 42 | } 43 | 44 | func main() { 45 | flag.Parse() 46 | config.Init(*flagConfigPath) 47 | logrus.SetLevel(logrus.DebugLevel) 48 | common.CurrentRuntime = common.Runtimes.Standalone 49 | 50 | model.Init(true) 51 | controller.Init() 52 | init_validators() 53 | 54 | fmt.Printf("Server now running on 0.0.0.0:%d", *flagPort) 55 | controller.Engine.Run(fmt.Sprintf("0.0.0.0:%d", *flagPort)) 56 | } 57 | -------------------------------------------------------------------------------- /validator/nextid/nextid_test.go: -------------------------------------------------------------------------------- 1 | package nextid 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "testing" 6 | "time" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/google/uuid" 10 | "github.com/nextdotid/proof_server/types" 11 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 12 | "github.com/nextdotid/proof_server/validator" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func GenerateNextIDTestData() (nextid *NextID, avatarSK, targetAvatarSK *ecdsa.PrivateKey) { 17 | avatar, avatarSK := mycrypto.GenerateSecp256k1Keypair() 18 | targetAvatar, targetAvatarSK := mycrypto.GenerateSecp256k1Keypair() 19 | nextid = &NextID{ 20 | &validator.Base{ 21 | Platform: types.Platforms.NextID, 22 | Previous: "", 23 | Action: types.Actions.Create, 24 | Pubkey: avatar, 25 | Identity: mycrypto.CompressedPubkeyHex(targetAvatar), 26 | AltID: "", 27 | ProofLocation: "", 28 | Signature: []byte{}, 29 | SignaturePayload: "", 30 | Text: "", 31 | Extra: map[string]string{"target_signature": ""}, 32 | CreatedAt: time.Now(), 33 | Uuid: uuid.New(), 34 | }, 35 | } 36 | 37 | nextid.SignaturePayload = nextid.GenerateSignPayload() 38 | nextid.Signature, _ = mycrypto.SignPersonal([]byte(nextid.SignaturePayload), avatarSK) 39 | targetSig, _ := mycrypto.SignPersonal([]byte(nextid.SignaturePayload), targetAvatarSK) 40 | nextid.Extra["target_signature"] = common.Bytes2Hex(targetSig) 41 | 42 | return nextid, avatarSK, targetAvatarSK 43 | } 44 | 45 | func Test_Validate(t *testing.T) { 46 | t.Run("success", func(t *testing.T) { 47 | nextid, _, _ := GenerateNextIDTestData() 48 | require.NoError(t, nextid.Validate()) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /validator/das/das_test.go: -------------------------------------------------------------------------------- 1 | package das 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/config" 8 | "github.com/nextdotid/proof_server/util" 9 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 10 | "github.com/nextdotid/proof_server/validator" 11 | "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func before_each(t *testing.T) { 16 | logrus.SetLevel(logrus.DebugLevel) 17 | config.Init("../../config/config.test.json") 18 | } 19 | 20 | func generate() Das { 21 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x03b0b5900f2106475027b9f80d249916baa3d0fb57071b9b41980a65868519f825") 22 | created_at, _ := util.TimestampStringToTime("1653842234") 23 | 24 | return Das{ 25 | Base: &validator.Base{ 26 | Previous: "", 27 | Action: "create", 28 | Pubkey: pubkey, 29 | Identity: "mitchatmask.bit", 30 | Platform: "dotbit", 31 | CreatedAt: created_at, 32 | Uuid: uuid.MustParse("e16a0021-80de-4d12-bea7-9cc021f5b847"), 33 | }, 34 | } 35 | } 36 | 37 | func Test_GeneratePostPayload(t *testing.T) { 38 | t.Run("success", func(t *testing.T) { 39 | before_each(t) 40 | 41 | das := generate() 42 | result := das.GeneratePostPayload() 43 | require.Contains(t, result["default"], "%SIG_BASE64%") 44 | }) 45 | } 46 | 47 | func Test_Validate(t *testing.T) { 48 | t.Run("success", func(t *testing.T) { 49 | before_each(t) 50 | 51 | das := generate() 52 | das.Identity = "mItCHaTmASk.BiT" 53 | require.Nil(t, das.Validate()) 54 | require.Greater(t, len(das.Signature), 10) 55 | require.Equal(t, "mitchatmask.bit", das.Identity) 56 | require.Equal(t, das.Identity, das.AltID) 57 | }) 58 | 59 | // Do not test validation by ProofLocation, since it is unnecessary. 60 | } 61 | -------------------------------------------------------------------------------- /headless/client.go: -------------------------------------------------------------------------------- 1 | package headless 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "path" 11 | 12 | "golang.org/x/xerrors" 13 | ) 14 | 15 | // HeadlessClient handles communication for headless browser service 16 | type HeadlessClient struct { 17 | url string 18 | client *http.Client 19 | } 20 | 21 | // NewHeadlessClient creates a new headless client 22 | func NewHeadlessClient(url string) *HeadlessClient { 23 | return &HeadlessClient{url, http.DefaultClient} 24 | } 25 | 26 | // Find find whether the target matching payload exists 27 | func (h *HeadlessClient) Find(ctx context.Context, payload *FindRequest) (string, error) { 28 | u, err := url.Parse(h.url) 29 | if err != nil { 30 | return "", xerrors.Errorf("%w", err) 31 | } 32 | 33 | u.Path = path.Join(u.Path, "/v1/find") 34 | body, err := json.Marshal(payload) 35 | if err != nil { 36 | return "", xerrors.Errorf("%w", err) 37 | } 38 | 39 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body)) 40 | if err != nil { 41 | return "", xerrors.Errorf("%w", err) 42 | } 43 | 44 | req.Header.Add("Content-Type", "application/json") 45 | 46 | res, err := h.client.Do(req) 47 | if res != nil && err != nil { 48 | if _, err := io.Copy(io.Discard, res.Body); err != nil { 49 | return "", xerrors.Errorf("%w", err) 50 | } 51 | } 52 | 53 | if res != nil { 54 | defer res.Body.Close() 55 | } 56 | 57 | if err != nil { 58 | return "", xerrors.Errorf("%w", err) 59 | } 60 | 61 | contents, err := io.ReadAll(res.Body) 62 | if err != nil { 63 | return "", xerrors.Errorf("%w", err) 64 | } 65 | 66 | var resBody FindRespond 67 | if err := json.Unmarshal(contents, &resBody); err != nil { 68 | return "", xerrors.Errorf("%w", err) 69 | } 70 | 71 | return resBody.Content, nil 72 | } 73 | -------------------------------------------------------------------------------- /util/base1024/decode.go: -------------------------------------------------------------------------------- 1 | package base1024 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | "strings" 6 | ) 7 | 8 | func DecodeString(s string) ([]byte, error) { 9 | trimTail := false 10 | if strings.HasSuffix(s, TAIL) { 11 | s = strings.TrimRight(s, TAIL) 12 | trimTail = true 13 | } 14 | arr := strings.Split(s, "") 15 | points := lo.Map(arr, func(point string, index int) int { 16 | return lo.IndexOf(Emojis, point) 17 | }) 18 | 19 | pointsLength := len(points) 20 | remainder := pointsLength % 4 21 | safe := pointsLength - remainder 22 | source := make([]int, 0) 23 | 24 | for i := 0; i <= safe; i += 4 { 25 | tmp := make([]int, 0) 26 | if i < pointsLength { // first 27 | tmp = append(tmp, points[i]>>2) 28 | } 29 | 30 | if i+1 < pointsLength { // second 31 | tmp = append(tmp, ((points[i]&0x3)<<6)|(points[i+1]>>4)) 32 | } else if i+1 == pointsLength { // second is last 33 | tmp = append(tmp, (points[i]&0x3)<<6) 34 | } 35 | 36 | if i+2 < pointsLength { // third 37 | tmp = append(tmp, ((points[i+1]&0xf)<<4)|(points[i+2]>>6)) 38 | } else if i+2 == pointsLength { // third is last 39 | tmp = append(tmp, (points[i+1]&0xf)<<4) 40 | } 41 | 42 | if i+3 < pointsLength { // forth 43 | tmp = append(tmp, ((points[i+2]&0x3f)<<2)|(points[i+3]>>8)) 44 | tmp = append(tmp, points[i+3]&0xff) 45 | } else if i+3 == pointsLength { // forth is last 46 | tmp = append(tmp, (points[i+2]&0x3f)<<2) 47 | } 48 | 49 | if i < safe { // Not last chunk 50 | source = append(source, tmp...) 51 | } else if i >= safe && remainder != 0 { // Last chunk, with remainder 52 | source = append(source, tmp[0:remainder]...) 53 | } 54 | } 55 | 56 | if trimTail { 57 | source = lo.DropRight(source, 1) 58 | } 59 | resList := lo.Map(source, func(x int, _ int) byte { 60 | return byte(x) 61 | }) 62 | 63 | return resList, nil 64 | } 65 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1713145326, 6 | "narHash": "sha256-m7+IWM6mkWOg22EC5kRUFCycXsXLSU7hWmHdmBfmC3s=", 7 | "rev": "53a2c32bc66f5ae41a28d7a9a49d321172af621e", 8 | "revCount": 557721, 9 | "type": "tarball", 10 | "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2311.557721%2Brev-53a2c32bc66f5ae41a28d7a9a49d321172af621e/018ee413-6e9c-72d4-be11-b9bef24c16bc/source.tar.gz" 11 | }, 12 | "original": { 13 | "type": "tarball", 14 | "url": "https://flakehub.com/f/NixOS/nixpkgs/%2A.tar.gz" 15 | } 16 | }, 17 | "root": { 18 | "inputs": { 19 | "nixpkgs": "nixpkgs", 20 | "utils": "utils" 21 | } 22 | }, 23 | "systems": { 24 | "locked": { 25 | "lastModified": 1681028828, 26 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 27 | "owner": "nix-systems", 28 | "repo": "default", 29 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "nix-systems", 34 | "repo": "default", 35 | "type": "github" 36 | } 37 | }, 38 | "utils": { 39 | "inputs": { 40 | "systems": "systems" 41 | }, 42 | "locked": { 43 | "lastModified": 1710146030, 44 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 45 | "owner": "numtide", 46 | "repo": "flake-utils", 47 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "numtide", 52 | "repo": "flake-utils", 53 | "type": "github" 54 | } 55 | } 56 | }, 57 | "root": "root", 58 | "version": 7 59 | } 60 | -------------------------------------------------------------------------------- /controller/main.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-gonic/gin" 8 | "github.com/nextdotid/proof_server/common" 9 | "github.com/nextdotid/proof_server/validator" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | Engine *gin.Engine 15 | l = logrus.WithFields(logrus.Fields{"module": "controller"}) 16 | ) 17 | 18 | type ErrorResponse struct { 19 | Message string `json:"message"` 20 | } 21 | 22 | func middlewareCors() gin.HandlerFunc { 23 | // * 24 | return cors.Default() 25 | } 26 | 27 | func Init() { 28 | if Engine != nil { 29 | return 30 | } 31 | 32 | Engine = gin.Default() 33 | Engine.Use(middlewareCors()) 34 | 35 | Engine.GET("/healthz", healthz) 36 | Engine.POST("/v1/proof/payload", proofPayload) 37 | Engine.POST("/v1/proof", proofUpload) 38 | Engine.GET("/v1/proof/exists", proofExists) 39 | Engine.GET("/v1/proof", proofQuery) 40 | Engine.GET("/v1/proofchain/changes", proofChainChanges) 41 | Engine.GET("/v1/proofchain", proofChainQuery) 42 | Engine.POST("/v1/proof/restore_pubkey", proofRestorePubkey) 43 | 44 | Engine.GET("/v1/subkey", subkeyQuery) 45 | Engine.POST("/v1/subkey/payload", subkeyPayload) 46 | Engine.POST("/v1/subkey", subkeySubmit) 47 | } 48 | 49 | func errorResp(c *gin.Context, error_code int, err error) { 50 | c.JSON(error_code, ErrorResponse{ 51 | Message: err.Error(), 52 | }) 53 | } 54 | 55 | func healthz(c *gin.Context) { 56 | platforms := make([]string, 0) 57 | for p := range validator.PlatformFactories { 58 | platforms = append(platforms, string(p)) 59 | } 60 | 61 | c.JSON(http.StatusOK, gin.H{ 62 | "hello": "proof service", 63 | "runtime": common.CurrentRuntime, 64 | "platforms": platforms, 65 | "environment": common.Environment, 66 | "revision": common.Revision, 67 | "built_at": common.BuildTime, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /controller/subkey_submit.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/base64" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/nextdotid/proof_server/model" 9 | "github.com/nextdotid/proof_server/types" 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | type subkeySubmitRequest struct { 14 | Avatar string `json:"avatar"` 15 | Algorithm types.SubkeyAlgorithm `json:"algorithm"` 16 | PublicKey string `json:"public_key"` 17 | RP_ID string `json:"rp_id"` 18 | Name string `json:"name"` 19 | Signature string `json:"signature"` 20 | } 21 | 22 | type subkeySubmitResponse struct { 23 | SignPayload string `json:"sign_payload"` 24 | } 25 | 26 | // POST /v1/subkey 27 | func subkeySubmit(c *gin.Context) { 28 | req := subkeySubmitRequest{} 29 | if err := c.BindJSON(&req); err != nil { 30 | errorResp(c, 400, err) 31 | return 32 | } 33 | 34 | subkey := model.Subkey{ 35 | CreatedAt: time.Now(), 36 | Name: req.Name, 37 | RP_ID: req.RP_ID, 38 | Avatar: req.Avatar, 39 | Algorithm: req.Algorithm, 40 | PublicKey: req.PublicKey, 41 | } 42 | payload, err := subkey.SignPayload() 43 | if err != nil { 44 | errorResp(c, 400, err) 45 | return 46 | } 47 | signature, err := base64.StdEncoding.DecodeString(req.Signature) 48 | if err != nil { 49 | errorResp(c, 400, xerrors.Errorf("Error when decoding signature: %w", err)) 50 | return 51 | } 52 | if err := subkey.ValidateSignature(payload, signature); err != nil { 53 | errorResp(c, 400, xerrors.Errorf("Error when validating signature: %w", err)) 54 | return 55 | } 56 | 57 | tx := model.DB.Create(&subkey) 58 | if tx.Error != nil { 59 | errorResp(c, 500, xerrors.Errorf("Error when saving subkey: %w", err)) 60 | return 61 | } 62 | 63 | c.JSON(200, subkeyPayloadResponse{ 64 | SignPayload: payload, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /validator/keybase/keybase_test.go: -------------------------------------------------------------------------------- 1 | package keybase 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/config" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func before_each(t *testing.T) { 17 | logrus.SetLevel(logrus.DebugLevel) 18 | config.Init("../../config/config.test.json") 19 | } 20 | 21 | func generate() Keybase { 22 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x02a68c664d4165a7abbb0b4221831153c5f3b0ecb6f994ba95c696eb64ca37eebc") 23 | created_at, _ := util.TimestampStringToTime("1647329002") 24 | 25 | return Keybase{ 26 | Base: &validator.Base{ 27 | Previous: "", 28 | Action: "create", 29 | Pubkey: pubkey, 30 | Identity: "nykma", 31 | Platform: types.Platforms.Keybase, 32 | CreatedAt: created_at, 33 | Uuid: uuid.MustParse("909ee81f-4c5e-4319-affa-90d95eca614d"), 34 | }, 35 | } 36 | } 37 | 38 | func Test_GeneratePostPayload(t *testing.T) { 39 | t.Run("success", func(t *testing.T) { 40 | before_each(t) 41 | 42 | kb := generate() 43 | result := kb.GeneratePostPayload() 44 | require.Contains(t, result["default"], "To validate") 45 | require.Contains(t, result["default"], mycrypto.CompressedPubkeyHex(kb.Pubkey)) 46 | require.Contains(t, result["default"], "%SIG_BASE64%") 47 | }) 48 | } 49 | 50 | func Test_Validate(t *testing.T) { 51 | t.Run("success", func(t *testing.T) { 52 | before_each(t) 53 | 54 | kb := generate() 55 | kb.Identity = "NYKma" 56 | require.Nil(t, kb.Validate()) 57 | require.Greater(t, len(kb.Signature), 10) 58 | require.Equal(t, "nykma", kb.Identity) 59 | require.Equal(t, kb.Identity, kb.AltID) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /cli/generate/upload.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/go-resty/resty/v2" 12 | "github.com/nextdotid/proof_server/config" 13 | "github.com/nextdotid/proof_server/controller" 14 | "github.com/nextdotid/proof_server/types" 15 | ) 16 | 17 | func UploadToProof(gp GenerateParams, personaPublicKey string, createAt string, uuid string, signature []byte, walletSignature []byte) { 18 | config.InitCliConfig() 19 | 20 | req := controller.ProofUploadRequest{ 21 | Action: types.Action(gp.Action), 22 | Platform: types.Platform(gp.Platform), 23 | Identity: strings.ToLower(gp.Identity), 24 | PublicKey: personaPublicKey, 25 | CreatedAt: createAt, 26 | Uuid: uuid, 27 | ProofLocation: "", 28 | } 29 | 30 | if types.Action(gp.Action) == types.Actions.Create && types.Platform(gp.Platform) != types.Platforms.Ethereum { 31 | input := bufio.NewScanner(os.Stdin) 32 | fmt.Println("Proof Location (find out how to get the proof location for each platform at README.md):") 33 | input.Scan() 34 | req.ProofLocation = input.Text() 35 | } 36 | 37 | req.Extra.Signature = base64.StdEncoding.EncodeToString((signature)) 38 | if types.Platform(gp.Platform) == types.Platforms.Ethereum { 39 | req.Extra.EthereumWalletSignature = base64.StdEncoding.EncodeToString((walletSignature)) 40 | } 41 | 42 | url := getUploadUrl() 43 | client := resty.New() 44 | resp, err := client.R().SetBody(req).EnableTrace().Post(url) 45 | 46 | if resp.StatusCode() == http.StatusCreated { 47 | fmt.Println("Upload succeed!!") 48 | } else { 49 | panic(fmt.Sprintf("Oops, some error occured. resp:%v err:%v", resp, err)) 50 | } 51 | os.Exit(0) 52 | } 53 | 54 | func getUploadUrl() string { 55 | return config.Viper.GetString("server.hostname") + config.Viper.GetString("server.upload_path") 56 | } 57 | -------------------------------------------------------------------------------- /cmd/lambda_headless/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # https://docs.docker.com/language/golang/build-images/ 4 | FROM golang:1.18-buster AS build 5 | 6 | WORKDIR /app 7 | 8 | COPY go.mod ./ 9 | COPY go.sum ./ 10 | RUN go mod download 11 | COPY . ./ 12 | 13 | # Remember to build your handler executable for Linux! 14 | # https://github.com/aws/aws-lambda-go/blob/main/README.md#building-your-function 15 | RUN env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ 16 | go build -o /main ./cmd/lambda_headless/main.go 17 | 18 | 19 | # Install chromium 20 | FROM public.ecr.aws/lambda/provided:al2 as chromium 21 | 22 | # install brotli, so we can decompress chromium 23 | # we don't have access to brotli out of the box, to install we first need epel 24 | # https://docs.fedoraproject.org/en-US/epel/#what_is_extra_packages_for_enterprise_linux_or_epel 25 | RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ 26 | yum -y install brotli && \ 27 | yum clean all 28 | 29 | # download chromium 30 | # s/o to https://github.com/alixaxel/chrome-aws-lambda for the binary 31 | RUN yum -y install wget && \ 32 | wget --progress=dot:giga https://raw.githubusercontent.com/alixaxel/chrome-aws-lambda/master/bin/chromium.br -O /chromium.br && \ 33 | yum clean all 34 | 35 | # decompress chromium 36 | RUN brotli -d /chromium.br 37 | 38 | # copy artifacts to a clean image 39 | FROM public.ecr.aws/lambda/provided:al2 40 | 41 | # install chromium dependencies 42 | RUN yum -y install \ 43 | libX11 \ 44 | nano \ 45 | unzip \ 46 | wget \ 47 | xclock \ 48 | xorg-x11-xauth \ 49 | xterm && \ 50 | yum clean all 51 | 52 | # copy in chromium from chromium stage 53 | COPY --from=chromium /chromium /opt/chromium 54 | 55 | # grant our program access to chromium 56 | RUN chmod 777 /opt/chromium 57 | 58 | # copy in lambda fn from build stage 59 | COPY --from=build /main /main 60 | 61 | ENTRYPOINT ["/main"] 62 | -------------------------------------------------------------------------------- /util/crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/crypto" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_ToPubkey(t *testing.T) { 13 | t.Run("secp256k1", func(t *testing.T) { 14 | pk, _ := GenerateSecp256k1Keypair() 15 | compressed := "0x" + common.Bytes2Hex(crypto.CompressPubkey(pk)) 16 | pkRecovered, err := StringToSecp256k1Pubkey(compressed) 17 | require.NoError(t, err) 18 | require.Equal(t, pk.X.String(), pkRecovered.X.String()) 19 | require.Equal(t, pk.Y.String(), pkRecovered.Y.String()) 20 | }) 21 | } 22 | 23 | func Test_Secp256k1_SignVerify(t *testing.T) { 24 | t.Run("success", func(t *testing.T) { 25 | payload := "test123" 26 | pk, sk := GenerateSecp256k1Keypair() 27 | signature, err := SignPersonal([]byte(payload), sk) 28 | assert.Nil(t, err) 29 | 30 | err = ValidatePersonalSignature(payload, signature, pk) 31 | assert.Nil(t, err) 32 | }) 33 | 34 | t.Run("fail if pubkey mismatch", func(t *testing.T) { 35 | payload := "test123" 36 | _, sk := GenerateSecp256k1Keypair() 37 | signature, _ := SignPersonal([]byte(payload), sk) 38 | 39 | new_pk, _ := GenerateSecp256k1Keypair() 40 | err := ValidatePersonalSignature(payload, signature, new_pk) 41 | assert.NotNil(t, err) 42 | }) 43 | 44 | t.Run("fail if payload mismatch", func(t *testing.T) { 45 | payload := "test123" 46 | pk, sk := GenerateSecp256k1Keypair() 47 | signature, _ := SignPersonal([]byte(payload), sk) 48 | 49 | err := ValidatePersonalSignature("foobar", signature, pk) 50 | assert.NotNil(t, err) 51 | }) 52 | 53 | t.Run("fail if signature mismatch", func(t *testing.T) { 54 | pk, sk := GenerateSecp256k1Keypair() 55 | signature, _ := SignPersonal([]byte("foobar"), sk) 56 | 57 | err := ValidatePersonalSignature("test123", signature, pk) 58 | assert.NotNil(t, err) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /model/avatar_alias.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | mapset "github.com/deckarep/golang-set/v2" 8 | "github.com/samber/lo" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | type AvatarAlias struct { 13 | ID int64 `gorm:"primarykey"` 14 | CreatedAt time.Time `gorm:"column:created_at"` 15 | 16 | // Avatar public key ("0xPUBKEY_COMPRESSED_HEXSTRING") 17 | Avatar string `gorm:"column:avatar;not null;index"` 18 | // Alias avatar of this avatar ("0xPUBKEY_COMPRESSED_HEXSTRING") 19 | Alias string `gorm:"column:alias;not null;index"` 20 | 21 | // ProofChain record which creates this binding 22 | ProofChainID int64 `gorm:"index"` 23 | // ProofChain record which creates this binding 24 | ProofChain ProofChain 25 | } 26 | 27 | func (AvatarAlias) TableName() string { 28 | return "alias" 29 | } 30 | 31 | // FindAllAliasByAvatar finds all alias of an avatar 32 | func FindAllAliasByAvatar(origAvatar string) ([]string, error) { 33 | originalAvatar := MarshalAvatar(origAvatar) 34 | if originalAvatar == "" { 35 | return nil, xerrors.Errorf("invalid avatar") 36 | } 37 | 38 | aliases := mapset.NewSet(originalAvatar) 39 | avatarsToQuery := mapset.NewSet(originalAvatar) 40 | 41 | for { 42 | aliasInstances := make([]AvatarAlias, 0) 43 | tx := ReadOnlyDB.Model(&AvatarAlias{}). 44 | Where( 45 | "avatar IN @avatarsToQuery OR alias IN @avatarsToQuery", 46 | sql.Named("avatarsToQuery", avatarsToQuery.ToSlice()), 47 | ). 48 | Find(&aliasInstances) 49 | if tx.Error != nil { 50 | return nil, tx.Error 51 | } 52 | avatarsToQuery.Clear() 53 | lo.ForEach(aliasInstances, func(avatarAlias AvatarAlias, index int) { 54 | if aliases.Add(avatarAlias.Alias) { 55 | avatarsToQuery.Add(avatarAlias.Alias) 56 | } 57 | if aliases.Add(avatarAlias.Avatar) { 58 | avatarsToQuery.Add(avatarAlias.Avatar) 59 | } 60 | }) 61 | if avatarsToQuery.Cardinality() == 0 { 62 | break 63 | } 64 | } 65 | aliases.Remove(originalAvatar) 66 | return aliases.ToSlice(), nil 67 | } 68 | -------------------------------------------------------------------------------- /controller/proof_chain.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/nextdotid/proof_server/model" 8 | "golang.org/x/xerrors" 9 | ) 10 | 11 | type ProofChainRequest struct { 12 | Avatar string `form:"avatar"` 13 | Page int `form:"page"` 14 | } 15 | 16 | type ProofChainResponse struct { 17 | Pagination ProofChainPaginationResponse `json:"pagination"` 18 | ProofChains []model.ProofChainItem `json:"proof_chain"` 19 | } 20 | 21 | type ProofChainPaginationResponse struct { 22 | Total int64 `json:"total"` 23 | Per int `json:"per"` 24 | Current int `json:"current"` 25 | Next int `json:"next"` 26 | } 27 | 28 | func proofChainQuery(c *gin.Context) { 29 | req := ProofChainRequest{} 30 | if err := c.BindQuery(&req); err != nil { 31 | errorResp(c, http.StatusBadRequest, xerrors.Errorf("Param error")) 32 | return 33 | } 34 | if len(req.Avatar) == 0 { 35 | errorResp(c, http.StatusBadRequest, xerrors.Errorf("Param missing")) 36 | return 37 | } 38 | 39 | list, pagination, err := performProofChainQuery(req) 40 | if err != nil { 41 | errorResp(c, http.StatusInternalServerError, xerrors.Errorf("Error in DB: %w", err)) 42 | return 43 | } 44 | 45 | c.JSON(http.StatusOK, ProofChainResponse{ 46 | Pagination: pagination, 47 | ProofChains: list, 48 | }) 49 | } 50 | 51 | func performProofChainQuery(req ProofChainRequest) ([]model.ProofChainItem, ProofChainPaginationResponse, error) { 52 | pagination := ProofChainPaginationResponse{ 53 | Total: 0, 54 | Per: PER_PAGE, 55 | Current: req.Page, 56 | Next: 0, 57 | } 58 | if pagination.Current <= 0 { // `page` param not provided. Set it to 1. 59 | pagination.Current = 1 60 | } 61 | offsetCount := pagination.Per * (pagination.Current - 1) 62 | 63 | total, rs, err := model.ProofChainFindByPersona(req.Avatar, false, offsetCount, pagination.Per) 64 | pagination.Total = total 65 | if total > int64(pagination.Per*pagination.Current) { 66 | pagination.Next = pagination.Current + 1 67 | } 68 | 69 | return rs, pagination, err 70 | } 71 | -------------------------------------------------------------------------------- /util/sqs/sqs.go: -------------------------------------------------------------------------------- 1 | package sqs 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/sqs" 9 | myconfig "github.com/nextdotid/proof_server/config" 10 | "github.com/sirupsen/logrus" 11 | "golang.org/x/xerrors" 12 | ) 13 | 14 | var ( 15 | api SQSSendMessageAPI 16 | queueUrl *string 17 | ) 18 | 19 | type SQSSendMessageAPI interface { 20 | GetQueueUrl(ctx context.Context, 21 | params *sqs.GetQueueUrlInput, 22 | optFns ...func(*sqs.Options)) (*sqs.GetQueueUrlOutput, error) 23 | 24 | SendMessage(ctx context.Context, 25 | params *sqs.SendMessageInput, 26 | optFns ...func(*sqs.Options)) (*sqs.SendMessageOutput, error) 27 | } 28 | 29 | func getQueueUrl(c context.Context, api SQSSendMessageAPI, input *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) { 30 | return api.GetQueueUrl(c, input) 31 | } 32 | 33 | func sendMessage(c context.Context, api SQSSendMessageAPI, input *sqs.SendMessageInput) (*sqs.SendMessageOutput, error) { 34 | return api.SendMessage(c, input) 35 | } 36 | 37 | func Init(cfg aws.Config) { 38 | queueName := myconfig.C.Sqs.QueueName 39 | if queueName == "" { 40 | logrus.Fatal("queue_name is not set") 41 | } 42 | 43 | api = sqs.NewFromConfig(cfg) 44 | 45 | gqInput := &sqs.GetQueueUrlInput{QueueName: aws.String(queueName)} 46 | result, err := getQueueUrl(context.TODO(), api, gqInput) 47 | if err != nil { 48 | logrus.Fatalf("error getting queue url: %v", err) 49 | } 50 | 51 | queueUrl = result.QueueUrl 52 | } 53 | 54 | func Send(msg interface{}) error { 55 | if api == nil { 56 | return xerrors.New("sqs is not initialized") 57 | } 58 | 59 | json, err := json.Marshal(msg) 60 | if err != nil { 61 | logrus.Errorf("error marshalling message: %v", err) 62 | return err 63 | } 64 | 65 | smInput := &sqs.SendMessageInput{ 66 | MessageBody: aws.String(string(json)), 67 | QueueUrl: queueUrl, 68 | } 69 | 70 | _, err = sendMessage(context.TODO(), api, smInput) 71 | if err != nil { 72 | logrus.Errorf("error sending message to sqs: %v", err) 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /controller/proof_chain_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_proofChainQuery(t *testing.T) { 10 | t.Run("success", func(t *testing.T) { 11 | before_each(t) 12 | insert_proof(t) 13 | resp_body := ProofChainResponse{} 14 | resp := APITestCall( 15 | Engine, 16 | "GET", 17 | fmt.Sprintf("/v1/proofchain?public_key=%s", persona), 18 | nil, 19 | &resp_body, 20 | ) 21 | assert.Equal(t, 200, resp.Code) 22 | //t.Logf("%s", resp.Body.String()) 23 | assert.Equal(t, 2, len(resp_body.ProofChains)) 24 | }) 25 | 26 | t.Run("empty result", func(t *testing.T) { 27 | before_each(t) 28 | resp_body := ProofChainResponse{} 29 | resp := APITestCall( 30 | Engine, 31 | "GET", 32 | fmt.Sprintf("/v1/proofchain?public_key=%s", "aaa"), 33 | nil, 34 | &resp_body, 35 | ) 36 | assert.Equal(t, 200, resp.Code) 37 | //t.Logf("%s", resp.Body.String()) 38 | assert.Equal(t, 0, len(resp_body.ProofChains)) 39 | }) 40 | 41 | t.Run("patination", func(t *testing.T) { 42 | before_each(t) 43 | for i := 0; i < 22; i++ { // Create 44 records 44 | insert_proof(t) 45 | } 46 | url := fmt.Sprintf("/v1/proofchain?public_key=%s", persona) 47 | 48 | resp_page1 := ProofChainResponse{} // Page not given 49 | APITestCall(Engine, "GET", url, nil, &resp_page1) 50 | assert.Equal(t, int64(44), resp_page1.Pagination.Total) 51 | assert.Equal(t, 1, resp_page1.Pagination.Current) 52 | assert.Equal(t, 2, resp_page1.Pagination.Next) 53 | assert.Equal(t, PER_PAGE, len(resp_page1.ProofChains)) 54 | 55 | resp_page3 := ProofChainResponse{} // Last page 56 | APITestCall(Engine, "GET", url+"&page=3", nil, &resp_page3) 57 | assert.Equal(t, 3, resp_page3.Pagination.Current) 58 | assert.Equal(t, 0, resp_page3.Pagination.Next) 59 | assert.Equal(t, 4, len(resp_page3.ProofChains)) 60 | 61 | resp_page4 := ProofChainResponse{} // Page overflow 62 | APITestCall(Engine, "GET", url+"&page=4", nil, &resp_page4) 63 | assert.Equal(t, 4, resp_page4.Pagination.Current) 64 | assert.Equal(t, 0, resp_page4.Pagination.Next) 65 | assert.Equal(t, 0, len(resp_page4.ProofChains)) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /headless/headless.go: -------------------------------------------------------------------------------- 1 | package headless 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-contrib/cors" 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-rod/rod" 10 | "github.com/go-rod/rod/lib/launcher" 11 | "github.com/nextdotid/proof_server/common" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var ( 16 | Engine *gin.Engine 17 | LauncherPath string 18 | l = logrus.WithFields(logrus.Fields{"module": "headless"}) 19 | URLReplacement = map[string]string{} 20 | Browser *rod.Browser 21 | ) 22 | 23 | func middlewareCors() gin.HandlerFunc { 24 | return cors.Default() 25 | } 26 | 27 | func Init(launcherPath string, urlReplacementRule string) { 28 | LauncherPath = launcherPath 29 | if Engine != nil { 30 | return 31 | } 32 | 33 | InitBrowser() 34 | InitUrlReplacementRule(urlReplacementRule) 35 | Engine = gin.Default() 36 | Engine.Use(middlewareCors()) 37 | 38 | Engine.GET("/healthz", healthz) 39 | Engine.POST("/v1/find", validate) 40 | } 41 | 42 | func healthz(c *gin.Context) { 43 | c.JSON(http.StatusOK, gin.H{ 44 | "hello": "proof service", 45 | "environment": common.Environment, 46 | "revision": common.Revision, 47 | "built_at": common.BuildTime, 48 | "runtime": common.CurrentRuntime, 49 | }) 50 | } 51 | 52 | func InitUrlReplacementRule(rule string) { 53 | if rule == "" { 54 | return 55 | } 56 | 57 | for _, r := range strings.Split(rule, ",") { 58 | parts := strings.Split(r, "=") 59 | if len(parts) != 2 { 60 | l.Warnf("invalid url replacement rule: %s", r) 61 | continue 62 | } 63 | 64 | URLReplacement[parts[0]] = parts[1] 65 | } 66 | } 67 | 68 | func InitBrowser() { 69 | var launcher *launcher.Launcher 70 | switch common.CurrentRuntime { 71 | case common.Runtimes.Lambda: 72 | launcher = newLambdaLauncher(LauncherPath) 73 | case common.Runtimes.Standalone: 74 | launcher = newLauncher(LauncherPath) 75 | } 76 | // defer launcher.Kill() 77 | // defer launcher.Cleanup() 78 | 79 | u, err := launcher.Launch() 80 | if err != nil { 81 | panic(err.Error()) 82 | } 83 | 84 | Browser = rod.New().ControlURL(u) 85 | // defer browser.Close() 86 | if err := Browser.Connect(); err != nil { 87 | panic(err.Error()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /validator/minds/minds_test.go: -------------------------------------------------------------------------------- 1 | package minds 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/nextdotid/proof_server/config" 9 | "github.com/nextdotid/proof_server/types" 10 | "github.com/nextdotid/proof_server/util" 11 | "github.com/nextdotid/proof_server/util/crypto" 12 | "github.com/nextdotid/proof_server/validator" 13 | "github.com/sirupsen/logrus" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func before_each(t *testing.T) { 18 | logrus.SetLevel(logrus.DebugLevel) 19 | config.Init("../../config/config.test.json") 20 | } 21 | 22 | func generate() Minds { 23 | pubkey, _ := crypto.StringToSecp256k1Pubkey("0x0398a22485635ed2262094103cfdc1511b785011e32a2eb16e5b32fd8561ea6ad8") 24 | created_at, _ := util.TimestampStringToTime("1664179121") 25 | uuid := uuid.MustParse("3d770975-5085-411b-91e4-661bcc407aa9") 26 | 27 | return Minds{ 28 | Base: &validator.Base{ 29 | Platform: types.Platforms.Minds, 30 | Previous: "", 31 | Action: types.Actions.Create, 32 | Pubkey: pubkey, 33 | Identity: "nykma", 34 | ProofLocation: "1421043369127186449", 35 | CreatedAt: created_at, 36 | Uuid: uuid, 37 | }, 38 | } 39 | } 40 | 41 | func Test_GeneratePostPayload(t *testing.T) { 42 | t.Run("success", func(t *testing.T) { 43 | before_each(t) 44 | 45 | minds := generate() 46 | post := minds.GeneratePostPayload() 47 | post_default, ok := post["default"] 48 | require.True(t, ok) 49 | require.Contains(t, post_default, "Verifying my Minds ID") 50 | require.Contains(t, post_default, minds.Identity) 51 | require.Contains(t, post_default, minds.Uuid.String()) 52 | require.Contains(t, post_default, "%SIG_BASE64%") 53 | }) 54 | } 55 | 56 | func Test_GenerateSignPayload(t *testing.T) { 57 | t.Run("success", func(t *testing.T) { 58 | before_each(t) 59 | 60 | minds := generate() 61 | payload := minds.GenerateSignPayload() 62 | require.Contains(t, payload, minds.Uuid.String()) 63 | require.Contains(t, payload, strconv.FormatInt(minds.CreatedAt.Unix(), 10)) 64 | require.Contains(t, payload, minds.Identity) 65 | }) 66 | } 67 | 68 | func Test_Validate(t *testing.T) { 69 | t.Run("success", func(t *testing.T) { 70 | before_each(t) 71 | 72 | minds := generate() 73 | require.NoError(t, minds.Validate()) 74 | require.Equal(t, "1302892485034381316", minds.AltID) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /controller/proof_exists.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/nextdotid/proof_server/model" 10 | "github.com/nextdotid/proof_server/util/crypto" 11 | "golang.org/x/xerrors" 12 | ) 13 | 14 | type ProofExistsRequest struct { 15 | Platform string `form:"platform"` 16 | Identity string `form:"identity"` 17 | PersonaPubkeyHex string `form:"public_key"` 18 | } 19 | 20 | type ProofExistsResponse struct { 21 | CreatedAt string `json:"created_at"` 22 | LastCheckedAt string `json:"last_checked_at"` 23 | IsValid bool `json:"is_valid"` 24 | InvalidReason string `json:"invalid_reason"` 25 | } 26 | 27 | func proofExists(c *gin.Context) { 28 | req := ProofExistsRequest{} 29 | if err := c.BindQuery(&req); err != nil { 30 | errorResp(c, http.StatusBadRequest, xerrors.Errorf("Param error")) 31 | return 32 | } 33 | if !proofExistsCheckRequest(&req) { 34 | errorResp(c, http.StatusBadRequest, xerrors.Errorf("Param missing")) 35 | return 36 | } 37 | 38 | personaPubkey, err := crypto.StringToSecp256k1Pubkey(req.PersonaPubkeyHex) 39 | if err != nil { 40 | errorResp(c, http.StatusBadRequest, xerrors.Errorf("Public key unmarshal error")) 41 | return 42 | } 43 | found := model.Proof{} 44 | tx := model.ReadOnlyDB.Where( 45 | "persona = ? AND platform = ? AND (identity = ? OR alt_id = ?)", 46 | model.MarshalAvatar(personaPubkey), 47 | req.Platform, 48 | strings.ToLower(req.Identity), 49 | strings.ToLower(req.Identity), 50 | ).Find(&found) 51 | 52 | if tx.Error != nil { 53 | errorResp(c, http.StatusInternalServerError, xerrors.Errorf("Error in DB: %w", err)) 54 | return 55 | } 56 | if found.ID == int64(0) { // Not found 57 | errorResp(c, http.StatusNotFound, xerrors.Errorf("Record not found for %s: %s", req.Platform, req.Identity)) 58 | return 59 | } 60 | if found.IsOutdated() { 61 | go triggerRevalidate(found.ID) 62 | } 63 | 64 | c.JSON(http.StatusOK, ProofExistsResponse{ 65 | CreatedAt: strconv.FormatInt(found.CreatedAt.Unix(), 10), 66 | LastCheckedAt: strconv.FormatInt(found.LastCheckedAt.Unix(), 10), 67 | IsValid: found.IsValid, 68 | InvalidReason: found.InvalidReason, 69 | }) 70 | } 71 | 72 | func proofExistsCheckRequest(req *ProofExistsRequest) bool { 73 | return req.Identity != "" && req.Platform != "" && req.PersonaPubkeyHex != "" 74 | } 75 | -------------------------------------------------------------------------------- /validator/discord/discord_test.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/config" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func before_each() { 17 | logrus.SetLevel(logrus.DebugLevel) 18 | config.Init("../../config/config.test.json") 19 | } 20 | 21 | func generate() Discord { 22 | pubkey, _ := crypto.StringToSecp256k1Pubkey("0x02d7c5e01bedf1c993f40ec302d9bf162620daea93a7155cd9a8019ae3a2c2a476") 23 | created_at, _ := util.TimestampStringToTime("1649299881") 24 | return Discord{ 25 | Base: &validator.Base{ 26 | Platform: types.Platforms.Discord, 27 | Previous: "", 28 | Action: types.Actions.Create, 29 | Pubkey: pubkey, 30 | Identity: "Sannie#0250", 31 | ProofLocation: "https://discord.com/channels/960708146706395176/960708146706395179/961458176719487076", 32 | CreatedAt: created_at, 33 | Uuid: uuid.MustParse("27b82012-bc83-4527-9351-9114e500d352"), 34 | }, 35 | } 36 | } 37 | 38 | func TestDiscord_Validate(t *testing.T) { 39 | t.Run("success", func(t *testing.T) { 40 | before_each() 41 | discord := generate() 42 | err := discord.Validate() 43 | assert.Nil(t, err) 44 | t.Logf("AltID: %s", discord.AltID) 45 | }) 46 | t.Run("different user", func(t *testing.T) { 47 | before_each() 48 | discord := generate() 49 | discord.Identity = "test#1234" 50 | err := discord.Validate() 51 | assert.NotNil(t, err) 52 | }) 53 | } 54 | 55 | func TestDiscord_GenerateSignPayload(t *testing.T) { 56 | t.Run("success", func(t *testing.T) { 57 | before_each() 58 | discord := generate() 59 | signPayload := discord.GenerateSignPayload() 60 | assert.Contains(t, signPayload, discord.Identity) 61 | assert.Contains(t, signPayload, string(discord.Action)) 62 | assert.Contains(t, signPayload, string(discord.Platform)) 63 | }) 64 | } 65 | 66 | func TestDiscord_GeneratePostPayload(t *testing.T) { 67 | t.Run("success", func(t *testing.T) { 68 | before_each() 69 | discord := generate() 70 | result := discord.GeneratePostPayload() 71 | assert.Contains(t, result["default"], discord.Identity) 72 | assert.Contains(t, result["default"], "%SIG_BASE64%") 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /validator/github/github_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/types" 8 | "github.com/nextdotid/proof_server/util" 9 | "github.com/nextdotid/proof_server/util/crypto" 10 | "github.com/nextdotid/proof_server/validator" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const ( 15 | test_pubkey = "0x03947957e8a8785b6520b96c1c0d70ae9cf59835eec18f9ac920bbf5733413366a" 16 | ) 17 | 18 | func generate() Github { 19 | pubkey, _ := crypto.StringToSecp256k1Pubkey(test_pubkey) 20 | created_at, _ := util.TimestampStringToTime("1647329002") 21 | return Github{ 22 | Base: &validator.Base{ 23 | Platform: types.Platforms.Github, 24 | Previous: "", 25 | Action: types.Actions.Create, 26 | Pubkey: pubkey, 27 | Identity: "nykma", 28 | ProofLocation: "5b3acc09d25242950e4b7ea0ee707ada", 29 | CreatedAt: created_at, 30 | Uuid: uuid.MustParse("909ee81f-4c5e-4319-affa-90d95eca614d"), 31 | }, 32 | } 33 | } 34 | 35 | func generate2() Github { 36 | pubkey, _ := crypto.StringToSecp256k1Pubkey("0x02d7c5e01bedf1c993f40ec302d9bf162620daea93a7155cd9a8019ae3a2c2a476") 37 | 38 | created_at, _ := util.TimestampStringToTime("1649060702") 39 | return Github{ 40 | Base: &validator.Base{ 41 | Platform: types.Platforms.Github, 42 | Previous: "", 43 | Action: types.Actions.Create, 44 | Pubkey: pubkey, 45 | Identity: "fengshanshan", 46 | ProofLocation: "31bb28bcf312b0eccd8202650b19e02e", 47 | CreatedAt: created_at, 48 | Uuid: uuid.MustParse("ca1e6a6f-3089-48d6-9214-74d9fb82bf82"), 49 | }, 50 | } 51 | } 52 | 53 | func Test_Validate(t *testing.T) { 54 | t.Run("success", func(t *testing.T) { 55 | github := generate() 56 | err := github.Validate() 57 | require.Nil(t, err) 58 | require.Equal(t, "1191636", github.AltID) 59 | }) 60 | 61 | t.Run("error if owner mismatch", func(t *testing.T) { 62 | github := generate() 63 | github.Identity = "foobar" 64 | 65 | err := github.Validate() 66 | require.NotNil(t, err) 67 | require.Contains(t, err.Error(), "gist owner mismatch") 68 | }) 69 | 70 | t.Run("error if gist is private", func(t *testing.T) { 71 | github := generate() 72 | github.ProofLocation = "a8acd06e99ae6baa4939300fc170446c" 73 | 74 | err := github.Validate() 75 | require.NotNil(t, err) 76 | require.Contains(t, err.Error(), "not found or empty") 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /validator/slack/slack_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/nextdotid/proof_server/config" 9 | "github.com/nextdotid/proof_server/types" 10 | "github.com/nextdotid/proof_server/util" 11 | "github.com/nextdotid/proof_server/util/crypto" 12 | "github.com/nextdotid/proof_server/validator" 13 | "github.com/sirupsen/logrus" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func before_each(t *testing.T) { 18 | logrus.SetLevel(logrus.DebugLevel) 19 | config.Init("../../config/config.test.json") 20 | } 21 | 22 | func generate() Slack { 23 | pubkey, _ := crypto.StringToSecp256k1Pubkey("0x4ec73e36f64ea6e2aa28c101dcae56203e02bd56b4b08c7848b5e791c7bfb9ca2b30f657bd822756533731e201faf57a0aaf6af36bd51f921f7132c9830c6fdf") 24 | created_at, _ := util.TimestampStringToTime("1677339048") 25 | uuid := uuid.MustParse("5032b8b3-d91d-434e-be3f-f172267e4006") 26 | 27 | return Slack{ 28 | Base: &validator.Base{ 29 | Platform: types.Platforms.Slack, 30 | Previous: "", 31 | Action: types.Actions.Create, 32 | Pubkey: pubkey, 33 | Identity: "ashfaqur", 34 | ProofLocation: "https://ashfaqur.slack.com/archives/C04Q3P6H7TK/p1677499644698189", 35 | CreatedAt: created_at, 36 | Uuid: uuid, 37 | }, 38 | } 39 | } 40 | 41 | func Test_GeneratePostPayload(t *testing.T) { 42 | t.Run("success", func(t *testing.T) { 43 | before_each(t) 44 | 45 | slack := generate() 46 | post := slack.GeneratePostPayload() 47 | post_default, ok := post["default"] 48 | require.True(t, ok) 49 | require.Contains(t, post_default, "Verifying my Slack ID") 50 | require.Contains(t, post_default, slack.Identity) 51 | require.Contains(t, post_default, slack.Uuid.String()) 52 | require.Contains(t, post_default, "%SIG_BASE64%") 53 | }) 54 | } 55 | 56 | func Test_GenerateSignPayload(t *testing.T) { 57 | t.Run("success", func(t *testing.T) { 58 | before_each(t) 59 | 60 | slack := generate() 61 | payload := slack.GenerateSignPayload() 62 | require.Contains(t, payload, slack.Uuid.String()) 63 | require.Contains(t, payload, strconv.FormatInt(slack.CreatedAt.Unix(), 10)) 64 | require.Contains(t, payload, slack.Identity) 65 | }) 66 | } 67 | 68 | func Test_Validate(t *testing.T) { 69 | t.Run("success", func(t *testing.T) { 70 | before_each(t) 71 | 72 | slack := generate() 73 | require.NoError(t, slack.Validate()) 74 | require.Equal(t, "U04Q3NRDWHX", slack.AltID) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /controller/proof_payload_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/nextdotid/proof_server/model" 9 | "github.com/nextdotid/proof_server/util/crypto" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_proofPayload(t *testing.T) { 14 | t.Run("success", func(t *testing.T) { 15 | before_each(t) 16 | req := ProofPayloadRequest{ 17 | Action: "create", 18 | Platform: "twitter", 19 | Identity: "yeiwb", 20 | PublicKey: "0x028c3cda474361179d653c41a62f6bbb07265d535121e19aedf660da2924d0b1e3", 21 | } 22 | resp := ProofPayloadResponse{} 23 | APITestCall(Engine, "POST", "/v1/proof/payload", &req, &resp) 24 | assert.Contains(t, resp.SignPayload, "\"action\":\"create\"") 25 | assert.Contains(t, resp.SignPayload, "\"platform\":\"twitter\"") 26 | assert.Contains(t, resp.SignPayload, "\"identity\":\"yeiwb\"") 27 | assert.Contains(t, resp.SignPayload, "\"prev\":null") 28 | 29 | assert.Contains(t, resp.PostContent["default"], "Verifying my Twitter ID") 30 | assert.Contains(t, resp.PostContent["default"], req.Identity) 31 | assert.Contains(t, resp.PostContent["default"], "Sig:") 32 | assert.Contains(t, resp.PostContent["default"], "%SIG_BASE64%") 33 | 34 | assert.True(t, len(resp.Uuid) > 0) 35 | assert.True(t, len(resp.CreatedAt) > 0) 36 | }) 37 | 38 | t.Run("with previous", func(t *testing.T) { 39 | before_each(t) 40 | pk, _ := crypto.StringToSecp256k1Pubkey("0x028c3cda474361179d653c41a62f6bbb07265d535121e19aedf660da2924d0b1e3") 41 | 42 | proof := model.ProofChain{ 43 | Persona: "0x" + crypto.CompressedPubkeyHex(pk), 44 | Platform: "twitter", 45 | Identity: "yeiwb", 46 | Location: "1469221200140574721", 47 | Signature: "gMUJ75eewkdaNrFp7bafzckv9+rlW7rVaxkB7/sYzYgFdFltYG+gn0lYzVNgrAdHWZPmu2giwJniGG7HG9iNigE=", 48 | } 49 | tx := model.DB.Create(&proof) 50 | assert.Nil(t, tx.Error) 51 | 52 | req := ProofPayloadRequest{ 53 | Action: "delete", 54 | Platform: "twitter", 55 | Identity: "yeiwb", 56 | PublicKey: "0x" + crypto.CompressedPubkeyHex(pk), 57 | } 58 | resp := ProofPayloadResponse{} 59 | 60 | APITestCall(Engine, "POST", "/v1/proof/payload", &req, &resp) 61 | sign_payload := gin.H{} 62 | 63 | assert.Nil(t, json.Unmarshal([]byte(resp.SignPayload), &sign_payload)) 64 | prev, ok := sign_payload["prev"] 65 | assert.True(t, ok) 66 | t.Logf("Prev: %s", prev) 67 | assert.Equal(t, prev, proof.Signature) 68 | }) 69 | 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/container.yaml: -------------------------------------------------------------------------------- 1 | name: Build docker containers 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | tags: 7 | - v*.*.* 8 | 9 | jobs: 10 | pack-image: 11 | runs-on: ubuntu-22.04 12 | env: 13 | IMAGE_NAME: ${{ github.repository }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Setup Go v1.x 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: '1.21' 21 | cache: true 22 | - name: Build (amd64) 23 | env: 24 | GOARCH: amd64 25 | GOOS: linux 26 | run: go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=production' -X 'github.com/nextdotid/proof_server/common.Revision=${{ github.sha }}' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" -o server_amd64 ./cmd/server 27 | - name: Build (arm64) 28 | env: 29 | GOARCH: arm64 30 | GOOS: linux 31 | run: go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=production' -X 'github.com/nextdotid/proof_server/common.Revision=${{ github.sha }}' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" -o server_arm64 ./cmd/server 32 | - name: Setup QEMU 33 | uses: docker/setup-qemu-action@v2 34 | - name: Setup Docker BuildX 35 | uses: docker/setup-buildx-action@v2 36 | with: 37 | platforms: linux/amd64,linux/arm64 38 | - name: Login in to Docker registry 39 | uses: docker/login-action@v2 40 | if: github.event_name != 'pull_request' 41 | with: 42 | username: ${{ secrets.DOCKER_USERNAME }} 43 | password: ${{ secrets.DOCKER_PASSWORD }} 44 | - name: Login in to Github registry 45 | uses: docker/login-action@v2 46 | if: github.event_name != 'pull_request' 47 | with: 48 | registry: ghcr.io 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | - name: Extract metadata 52 | id: meta 53 | uses: docker/metadata-action@v4 54 | with: 55 | images: | 56 | docker.io/${{ env.IMAGE_NAME }} 57 | ghcr.io/${{ env.IMAGE_NAME }} 58 | tags: | 59 | type=schedule 60 | type=ref,event=branch 61 | type=ref,event=pr 62 | type=semver,pattern={{version}} 63 | type=semver,pattern={{major}}.{{minor}} 64 | type=semver,pattern={{major}} 65 | - name: Build and push 66 | uses: docker/build-push-action@v4 67 | with: 68 | context: '.' 69 | push: ${{ github.event_name != 'pull_request' }} 70 | file: ./.github/workflows/docker/Dockerfile 71 | tags: ${{ steps.meta.outputs.tags }} 72 | labels: ${{ steps.meta.outputs.labels }} 73 | platforms: linux/amd64,linux/arm64 74 | -------------------------------------------------------------------------------- /headless/launcher.go: -------------------------------------------------------------------------------- 1 | package headless 2 | 3 | import "github.com/go-rod/rod/lib/launcher" 4 | 5 | // newLauncher creates a new launcher with default options. 6 | func newLauncher(path string) *launcher.Launcher { 7 | if path == "" { 8 | var found bool 9 | 10 | path, found = launcher.LookPath() 11 | if !found { 12 | path = launcher.NewBrowser().MustGet() 13 | } 14 | } 15 | 16 | return launcher.New(). 17 | Bin(path). 18 | Leakless(false). 19 | Headless(true). 20 | Logger(l.Writer()). 21 | Set("--allow-running-insecure-content"). 22 | Set("--autoplay-policy", "user-gesture-required"). 23 | Set("--disable-component-update"). 24 | Set("--disable-domain-reliability"). 25 | Set("--disable-features", "AudioServiceOutOfProcess", "IsolateOrigins", "site-per-process"). 26 | Set("--disable-print-preview"). 27 | Set("--disable-setuid-sandbox"). 28 | Set("--disable-site-isolation-trials"). 29 | Set("--disable-speech-api"). 30 | Set("--disable-web-security"). 31 | // Set("--disk-cache-size", "33554432"). 32 | Set("--enable-features", "SharedArrayBuffer"). 33 | Set("--hide-scrollbars"). 34 | // Set("--ignore-gpu-blocklist"). 35 | // Set("--in-process-gpu"). 36 | Set("--mute-audio"). 37 | Set("--no-default-browser-check"). 38 | Set("--no-pings"). 39 | // Set("--no-sandbox"). 40 | // Set("--no-zygote"). 41 | // Set("--single-process"). 42 | // Set("--use-gl", "swiftshader"). 43 | Set("--window-size", "1920", "1080"). 44 | Delete("--no-startup-window") 45 | } 46 | 47 | // newLambdaLauncher creates a new launcher with default options for AWS Lambda. 48 | func newLambdaLauncher(path string) *launcher.Launcher { 49 | if path == "" { 50 | var found bool 51 | 52 | path, found = launcher.LookPath() 53 | if !found { 54 | path = launcher.NewBrowser().MustGet() 55 | } 56 | } 57 | 58 | return launcher.New(). 59 | Bin(path). 60 | // recommended flags to run in serverless environments 61 | // see https://github.com/alixaxel/chrome-aws-lambda/blob/master/source/index.ts 62 | Set("allow-running-insecure-content"). 63 | Set("autoplay-policy", "user-gesture-required"). 64 | Set("disable-component-update"). 65 | Set("disable-domain-reliability"). 66 | Set("disable-features", "AudioServiceOutOfProcess", "IsolateOrigins", "site-per-process"). 67 | Set("disable-print-preview"). 68 | Set("disable-setuid-sandbox"). 69 | Set("disable-site-isolation-trials"). 70 | Set("disable-speech-api"). 71 | Set("disable-web-security"). 72 | Set("disk-cache-size", "33554432"). 73 | Set("enable-features", "SharedArrayBuffer"). 74 | Set("hide-scrollbars"). 75 | Set("ignore-gpu-blocklist"). 76 | Set("in-process-gpu"). 77 | Set("mute-audio"). 78 | Set("no-default-browser-check"). 79 | Set("no-pings"). 80 | Set("no-sandbox"). 81 | Set("no-zygote"). 82 | Set("single-process"). 83 | Set("use-gl", "swiftshader"). 84 | Set("window-size", "1920", "1080") 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/container_headless.yaml: -------------------------------------------------------------------------------- 1 | name: Build headless browser docker containers 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | tags: 7 | - v*.*.* 8 | 9 | jobs: 10 | pack-headless-image: 11 | runs-on: ubuntu-22.04 12 | env: 13 | IMAGE_NAME: ${{ github.repository }}_headless 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Setup Go v1.x 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: '1.21' 21 | cache: true 22 | - name: Build (amd64) 23 | env: 24 | GOARCH: amd64 25 | GOOS: linux 26 | CGO_ENABLED: '0' 27 | run: go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=production' -X 'github.com/nextdotid/proof_server/common.Revision=${{ github.sha }}' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" -o headless_amd64 ./cmd/headless 28 | - name: Build (arm64) 29 | env: 30 | GOARCH: arm64 31 | GOOS: linux 32 | CGO_ENABLED: '0' 33 | run: go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=production' -X 'github.com/nextdotid/proof_server/common.Revision=${{ github.sha }}' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" -o headless_arm64 ./cmd/headless 34 | - name: Setup QEMU 35 | uses: docker/setup-qemu-action@v2 36 | - name: Setup Docker BuildX 37 | uses: docker/setup-buildx-action@v2 38 | with: 39 | platforms: linux/amd64,linux/arm64 40 | - name: Login in to Docker registry 41 | uses: docker/login-action@v2 42 | if: github.event_name != 'pull_request' 43 | with: 44 | username: ${{ secrets.DOCKER_USERNAME }} 45 | password: ${{ secrets.DOCKER_PASSWORD }} 46 | - name: Login in to Github registry 47 | uses: docker/login-action@v2 48 | if: github.event_name != 'pull_request' 49 | with: 50 | registry: ghcr.io 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | - name: Extract metadata 54 | id: meta 55 | uses: docker/metadata-action@v4 56 | with: 57 | images: | 58 | docker.io/${{ env.IMAGE_NAME }} 59 | ghcr.io/${{ env.IMAGE_NAME }} 60 | tags: | 61 | type=schedule 62 | type=ref,event=branch 63 | type=ref,event=pr 64 | type=semver,pattern={{version}} 65 | type=semver,pattern={{major}}.{{minor}} 66 | type=semver,pattern={{major}} 67 | - name: Build and push 68 | uses: docker/build-push-action@v4 69 | with: 70 | context: '.' 71 | push: ${{ github.event_name != 'pull_request' }} 72 | file: ./.github/workflows/docker/Dockerfile.headless 73 | tags: ${{ steps.meta.outputs.tags }} 74 | labels: ${{ steps.meta.outputs.labels }} 75 | platforms: linux/amd64,linux/arm64 76 | -------------------------------------------------------------------------------- /model/proof.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/nextdotid/proof_server/types" 8 | "github.com/nextdotid/proof_server/validator" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | // EXPIRED_IN is the time after which a proof is considered expired and should perform revalidate. 13 | const EXPIRED_IN = time.Hour * 24 * 3 14 | 15 | // Proof is final proof state of a user (persona). 16 | type Proof struct { 17 | ID int64 `gorm:"primarykey"` 18 | CreatedAt time.Time 19 | LastCheckedAt time.Time 20 | IsValid bool 21 | InvalidReason string 22 | 23 | ProofChainID int64 `gorm:"index"` 24 | ProofChain ProofChain 25 | // Persona is public key of user persona (string, /0x[0-9a-f]{130}/) 26 | Persona string `gorm:"index;not null"` 27 | Platform types.Platform `gorm:"index;not null"` 28 | Identity string `gorm:"index;not null"` 29 | AltID string `gorm:"column:alt_id;index"` 30 | Location string `gorm:"not null"` 31 | } 32 | 33 | func (Proof) TableName() string { 34 | return "proof" 35 | } 36 | 37 | func FindAllProofByPersona(persona any, orderBy string) (proofs []Proof, err error) { 38 | marshaled_persona := MarshalAvatar(persona) 39 | proofs = make([]Proof, 0) 40 | tx := ReadOnlyDB.Model(&Proof{}).Where("persona = ?", marshaled_persona).Order(orderBy).Find(&proofs) 41 | if tx.Error != nil { 42 | return nil, xerrors.Errorf("error when finding proofs: %w", err) 43 | } 44 | return proofs, nil 45 | } 46 | 47 | // IsOutdated returns true if proof is outdated and should do a revalidate. 48 | func (proof *Proof) IsOutdated() bool { 49 | return proof.LastCheckedAt.Add(EXPIRED_IN).Before(time.Now()) 50 | } 51 | 52 | // Revalidate validates current proof, will update `IsValid` and 53 | // `LastCheckedAt`. Must be used after `DB.Preload("ProofChain")`. 54 | func (proof *Proof) Revalidate() (err error) { 55 | // FIXME: platform twitter causes too many errors. Disable for now. 56 | if proof.Platform == types.Platforms.Twitter { 57 | return nil 58 | } 59 | 60 | v, err := proof.ProofChain.RestoreValidator() 61 | if err != nil || v == nil { 62 | return xerrors.Errorf("restoring validator: %w", err) 63 | } 64 | 65 | iv := validator.BaseToInterface(v) 66 | if iv == nil { 67 | return xerrors.Errorf("unknown platform: %s", string(proof.Platform)) 68 | } 69 | 70 | err = iv.Validate() 71 | if err != nil { 72 | proof.touchValid(err.Error(), iv.GetAltID()) 73 | return xerrors.Errorf("validate failed: %w", err) 74 | } 75 | 76 | proof.touchValid("", iv.GetAltID()) 77 | // TODO: need to update `identity` and `alt_id` here. 78 | return nil 79 | } 80 | 81 | func (proof *Proof) touchValid(reason, altID string) { 82 | fmt.Printf("AltID: %s\n", altID) 83 | proof.LastCheckedAt = time.Now() 84 | proof.IsValid = (reason == "") 85 | proof.InvalidReason = reason 86 | if altID != "" { 87 | proof.AltID = altID 88 | } 89 | 90 | DB.Save(proof) 91 | } 92 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "ProofService: NextID's identity binding provider"; 3 | 4 | # Nixpkgs / NixOS version to use. 5 | inputs = { 6 | nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/*.tar.gz"; 7 | utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, utils }: 11 | let 12 | 13 | # to work with older version of flakes 14 | lastModifiedDate = self.lastModifiedDate or self.lastModified or "19700101"; 15 | 16 | # Generate a user-friendly version number. 17 | version = builtins.substring 0 8 lastModifiedDate; 18 | 19 | # System types to support. 20 | supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; 21 | 22 | # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. 23 | forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 24 | 25 | # Nixpkgs instantiated for supported system types. 26 | nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); 27 | 28 | in 29 | { 30 | 31 | # Provide some binary packages for selected system types. 32 | packages = forAllSystems (system: 33 | let 34 | pkgs = nixpkgsFor.${system}; 35 | in 36 | { 37 | proof-server = pkgs.buildGoModule { 38 | pname = "proof-server"; 39 | inherit version; 40 | # In 'nix develop', we don't need a copy of the source tree 41 | # in the Nix store. 42 | src = ./.; 43 | 44 | # This hash locks the dependencies of this package. It is 45 | # necessary because of how Go requires network access to resolve 46 | # VCS. See https://www.tweag.io/blog/2021-03-04-gomod2nix/ for 47 | # details. Normally one can build with a fake sha256 and rely on native Go 48 | # mechanisms to tell you what the hash should be or determine what 49 | # it should be "out-of-band" with other tooling (eg. gomod2nix). 50 | # To begin with it is recommended to set this, but one must 51 | # remeber to bump this hash when your dependencies change. 52 | vendorSha256 = pkgs.lib.fakeSha256; 53 | 54 | #vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo="; 55 | }; 56 | }); 57 | 58 | # Add dependencies that are only needed for development 59 | devShells = forAllSystems (system: 60 | let 61 | pkgs = nixpkgsFor.${system}; 62 | in 63 | { 64 | default = pkgs.mkShell { 65 | buildInputs = with pkgs; [ go gopls gotools go-tools delve ]; 66 | }; 67 | }); 68 | 69 | # The default package for 'nix build'. This makes sense if the 70 | # flake provides only one package or there is a clear "main" 71 | # package. 72 | defaultPackage = forAllSystems (system: self.packages.${system}.proof-server); 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /validator/ens/ens_test.go: -------------------------------------------------------------------------------- 1 | package ens 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func build() ENS { 16 | pk, _ := crypto.StringToSecp256k1Pubkey("0x028568e07ebf497b07a30f8a9d1731980736a4fac9d7c9c9b5682cb82dd3e774d7") 17 | createdAt, _ := util.TimestampStringToTime("1664267795") 18 | return ENS{ 19 | Base: &validator.Base{ 20 | Platform: types.Platforms.ENS, 21 | Previous: "", 22 | Action: types.Actions.Create, 23 | Pubkey: pk, 24 | Identity: "testcase.nextnext.id", 25 | CreatedAt: createdAt, 26 | Uuid: uuid.MustParse("80c98711-f4f6-43c7-b05c-8d86372f6131"), 27 | }, 28 | } 29 | } 30 | 31 | func build_invalid() ENS { 32 | ens := build() 33 | ens.Identity = "testcase_invalid.nextnext.id" 34 | return ens 35 | } 36 | 37 | func Test_parseTxt(t *testing.T) { 38 | t.Run("success", func(t *testing.T) { 39 | txt := "ps:true;v:1;sig:Oyist/0E0MJ5sN3TI33P4EMBGTaCk2S3IQKzYfI5zxpwE2VdHClgLXfmj0L2dPydF8KOXyjbWWuM2AHKdW2DnwE=;ca:1664263102;uuid:26de5ec2-889e-4f59-9fac-dfa8d99e7ce7;prev:null" 40 | result, err := parseTxt(txt) 41 | require.NoError(t, err) 42 | require.Equal(t, "Oyist/0E0MJ5sN3TI33P4EMBGTaCk2S3IQKzYfI5zxpwE2VdHClgLXfmj0L2dPydF8KOXyjbWWuM2AHKdW2DnwE=", result.Signature) 43 | require.Nil(t, result.Previous) 44 | }) 45 | 46 | t.Run("struct field missing", func(t *testing.T) { 47 | txt := "ps:true;v:1;sig:Oyist/0E0MJ5sN3TI33P4EMBGTaCk2S3IQKzYfI5zxpwE2VdHClgLXfmj0L2dPydF8KOXyjbWWuM2AHKdW2DnwE=;ca:1664263102;uuid:26de5ec2-889e-4f59-9fac-dfa8d99e7ce7" 48 | _, err := parseTxt(txt) 49 | require.Error(t, err) 50 | }) 51 | } 52 | 53 | func Test_GeneratePostPayload(t *testing.T) { 54 | t.Run("success", func(t *testing.T) { 55 | ens := build() 56 | payload_map := ens.GeneratePostPayload() 57 | payload := payload_map["default"] 58 | require.Contains(t, payload, ens.Uuid.String()) 59 | require.Contains(t, payload, strconv.FormatInt(ens.CreatedAt.Unix(), 10)) 60 | require.Contains(t, payload, "%SIG_BASE64%") 61 | require.Contains(t, payload, "prev:null") 62 | }) 63 | } 64 | 65 | func Test_GenerateSignPayload(t *testing.T) { 66 | t.Run("success", func(t *testing.T) { 67 | ens := build() 68 | sp := ens.GenerateSignPayload() 69 | require.Contains(t, sp, types.Platforms.ENS) 70 | require.Contains(t, sp, types.Actions.Create) 71 | require.Contains(t, sp, ens.Uuid.String()) 72 | }) 73 | } 74 | 75 | func Test_Validate(t *testing.T) { 76 | t.Run("success", func(t *testing.T) { 77 | ens := build() 78 | require.NoError(t, ens.Validate()) 79 | require.Equal(t, ens.Identity, ens.AltID) 80 | }) 81 | 82 | t.Run("invalid", func(t *testing.T) { 83 | ens := build_invalid() 84 | err := ens.Validate() 85 | require.Error(t, err) 86 | t.Log(err.Error()) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /cli/query/query.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/go-resty/resty/v2" 12 | "github.com/nextdotid/proof_server/config" 13 | ) 14 | 15 | type QueryParams struct { 16 | Platform string `json:"platform"` 17 | Identity string `json:"identity"` // Identity on target platform. 18 | Page string `json:"page"` 19 | } 20 | 21 | func QueryProof() { 22 | config.InitCliConfig() 23 | params := initParams() 24 | req := make(map[string]string) 25 | req["platform"] = params.Platform 26 | req["identity"] = params.Identity 27 | req["page"] = params.Page 28 | getAndPrintData(req) 29 | 30 | input := bufio.NewScanner(os.Stdin) 31 | for { 32 | fmt.Println("\nChoose next step:\n 1. next page\n 2. Get the data of the specified page\n 3. Quit\n Enter the number:") 33 | input.Scan() 34 | nextStep := input.Text() 35 | switch nextStep { 36 | case "1": 37 | page, _ := strconv.Atoi(params.Page) 38 | page += 1 39 | fmt.Printf("\nGet the data of Page %d\n", page) 40 | req["page"] = strconv.FormatInt(int64(page), 10) 41 | getAndPrintData(req) 42 | case "2": 43 | fmt.Println("\nPlease enter the page number") 44 | input.Scan() 45 | page := input.Text() 46 | req["page"] = page 47 | getAndPrintData(req) 48 | case "3": 49 | os.Exit(0) 50 | default: 51 | panic(fmt.Sprintf("Unknown Operation %s", nextStep)) 52 | } 53 | } 54 | } 55 | 56 | func getAndPrintData(req map[string]string) { 57 | url := getQueryUrl() 58 | client := resty.New() 59 | resp, err := client.R().SetQueryParams(req).EnableTrace().Get(url) 60 | if err != nil { 61 | panic(fmt.Sprintf("Oops, fail to get the result, err:%v", err)) 62 | } 63 | fmt.Println(PrettyString(string(resp.Body()))) 64 | } 65 | 66 | func initParams() QueryParams { 67 | input := bufio.NewScanner(os.Stdin) 68 | fmt.Println("For the query process, we could have platform/identity/page as the query condition\n") 69 | 70 | fmt.Println("Platform (find out a support platform at README.md):") 71 | input.Scan() 72 | platform := input.Text() 73 | 74 | fmt.Println("\nIdentity (find out the identity of each platform at README.md):") 75 | input.Scan() 76 | identity := input.Text() 77 | 78 | fmt.Println("\nPage (We will give maximum 20 results for each query, you can give a page number for getting more results):") 79 | input.Scan() 80 | page := input.Text() 81 | 82 | return QueryParams{ 83 | Platform: platform, 84 | Identity: identity, 85 | Page: page, 86 | } 87 | } 88 | 89 | func getQueryUrl() string { 90 | return config.Viper.GetString("server.hostname") + config.Viper.GetString("server.query_path") 91 | } 92 | 93 | func PrettyString(str string) (string, error) { 94 | var prettyJSON bytes.Buffer 95 | if err := json.Indent(&prettyJSON, []byte(str), "", " "); err != nil { 96 | return "", err 97 | } 98 | return prettyJSON.String(), nil 99 | } 100 | -------------------------------------------------------------------------------- /validator/nextid/nextid.go: -------------------------------------------------------------------------------- 1 | package nextid 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/common/hexutil" 9 | "github.com/nextdotid/proof_server/types" 10 | "github.com/nextdotid/proof_server/util" 11 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 12 | "github.com/nextdotid/proof_server/validator" 13 | "github.com/sirupsen/logrus" 14 | "golang.org/x/xerrors" 15 | ) 16 | 17 | type NextID struct { 18 | *validator.Base 19 | } 20 | 21 | var ( 22 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "nextid"}) 23 | ) 24 | 25 | func Init() { 26 | if validator.PlatformFactories == nil { 27 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 28 | } 29 | 30 | validator.PlatformFactories[types.Platforms.NextID] = func(base *validator.Base) validator.IValidator { 31 | nextID := NextID{base} 32 | return &nextID 33 | } 34 | } 35 | 36 | func (nextID *NextID) GetAltID() (altID string) { 37 | return "" 38 | } 39 | 40 | func (nextID *NextID) GeneratePostPayload() (post map[string]string) { 41 | return map[string]string{ 42 | "default": "", 43 | } 44 | } 45 | 46 | // GenerateSignPayload generates a string to be signed. If empty, an error is occured internally. 47 | func (nextID *NextID) GenerateSignPayload() (payload string) { 48 | targetAvatar, err := mycrypto.StringToSecp256k1Pubkey(nextID.Identity) 49 | if err != nil { 50 | return "" 51 | } 52 | 53 | payloadStruct := validator.H{ 54 | "action": string(nextID.Action), 55 | "identity": "0x" + mycrypto.CompressedPubkeyHex(targetAvatar), 56 | "persona": "0x" + mycrypto.CompressedPubkeyHex(nextID.Pubkey), 57 | "platform": "nextid", 58 | "prev": nil, 59 | "created_at": util.TimeToTimestampString(nextID.CreatedAt), 60 | "uuid": nextID.Uuid.String(), 61 | } 62 | if nextID.Previous != "" { 63 | payloadStruct["prev"] = nextID.Previous 64 | } 65 | payloadBytes, err := json.Marshal(payloadStruct) 66 | if err != nil { 67 | l.Warnf("Error when marshaling struct: %s", err.Error()) 68 | return "" 69 | } 70 | return string(payloadBytes) // TODO 71 | } 72 | 73 | func (nextID *NextID) Validate() (err error) { 74 | targetSig, ok := nextID.Extra["target_signature"] 75 | if !ok { 76 | return xerrors.Errorf("Target Avatar signature not provided") 77 | } 78 | targetSigParsed := strings.TrimPrefix(targetSig, "0x") 79 | targetSigParsed = strings.ToLower(targetSigParsed) 80 | targetSigBytes := common.Hex2Bytes(targetSigParsed) 81 | 82 | targetAvatar, err := mycrypto.StringToSecp256k1Pubkey(nextID.Identity) 83 | if err != nil { 84 | return xerrors.Errorf("Invalid target avatar: %s", nextID.Identity) 85 | } 86 | 87 | hexutil.Decode(targetSig) 88 | payload := nextID.GenerateSignPayload() 89 | if err := mycrypto.ValidatePersonalSignature(payload, nextID.Signature, nextID.Pubkey); err != nil { 90 | return xerrors.Errorf("Invalid base signature: %w", err) 91 | } 92 | if err := mycrypto.ValidatePersonalSignature(payload, targetSigBytes, targetAvatar); err != nil { 93 | return xerrors.Errorf("Invalid target signature: %w", err) 94 | } 95 | 96 | return nil // TODO 97 | } 98 | -------------------------------------------------------------------------------- /controller/proof_payload.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/google/uuid" 9 | "github.com/nextdotid/proof_server/model" 10 | "github.com/nextdotid/proof_server/types" 11 | "github.com/nextdotid/proof_server/util" 12 | "github.com/nextdotid/proof_server/util/crypto" 13 | "github.com/nextdotid/proof_server/validator" 14 | "golang.org/x/xerrors" 15 | ) 16 | 17 | type ProofPayloadRequest struct { 18 | Action types.Action `json:"action"` 19 | Platform types.Platform `json:"platform"` 20 | Identity string `json:"identity"` 21 | PublicKey string `json:"public_key"` 22 | Extra ProofPayloadRequestExtra `json:"extra"` 23 | } 24 | 25 | type ProofPayloadResponse struct { 26 | PostContent map[string]string `json:"post_content"` 27 | SignPayload string `json:"sign_payload"` 28 | Uuid string `json:"uuid"` 29 | CreatedAt string `json:"created_at"` 30 | } 31 | 32 | type ProofPayloadRequestExtra struct { 33 | EthereumWalletSignature string `json:"wallet_signature"` 34 | } 35 | 36 | func proofPayload(c *gin.Context) { 37 | req := &ProofPayloadRequest{} 38 | err := c.BindJSON(req) 39 | if err != nil { 40 | errorResp(c, http.StatusBadRequest, xerrors.Errorf("when parsing body: %w", err)) 41 | return 42 | } 43 | if !proofPayloadCheckRequest(req) { 44 | errorResp(c, http.StatusBadRequest, xerrors.New("param invalid")) 45 | return 46 | } 47 | 48 | parsed_pubkey, err := crypto.StringToSecp256k1Pubkey(req.PublicKey) 49 | if err != nil { 50 | errorResp(c, http.StatusBadRequest, xerrors.New("public key not recognized")) 51 | return 52 | } 53 | 54 | previous_pc, err := model.ProofChainFindLatest(crypto.CompressedPubkeyHex(parsed_pubkey)) 55 | if err != nil { 56 | errorResp(c, http.StatusInternalServerError, xerrors.New("previous proof not found")) 57 | return 58 | } 59 | 60 | var previous_signature string 61 | if previous_pc == nil { 62 | previous_signature = "" 63 | } else { 64 | previous_signature = previous_pc.Signature 65 | } 66 | 67 | v := validator.Base{ 68 | Platform: req.Platform, 69 | Previous: previous_signature, 70 | Action: req.Action, 71 | Pubkey: parsed_pubkey, 72 | Identity: req.Identity, 73 | Uuid: uuid.New(), 74 | CreatedAt: time.Now(), 75 | Extra: map[string]string{ 76 | "wallet_signature": req.Extra.EthereumWalletSignature, 77 | }, 78 | } 79 | 80 | performer := validator.BaseToInterface(&v) 81 | if performer == nil { 82 | errorResp(c, http.StatusBadRequest, xerrors.New("unknown platform")) 83 | return 84 | } 85 | c.JSON(http.StatusOK, ProofPayloadResponse{ 86 | PostContent: performer.GeneratePostPayload(), 87 | SignPayload: performer.GenerateSignPayload(), 88 | CreatedAt: util.TimeToTimestampString(v.CreatedAt), 89 | Uuid: v.Uuid.String(), 90 | }) 91 | } 92 | 93 | func proofPayloadCheckRequest(req *ProofPayloadRequest) bool { 94 | return string(req.Action) != "" && 95 | req.Platform != "" && 96 | req.Identity != "" && 97 | req.PublicKey != "" 98 | 99 | } 100 | -------------------------------------------------------------------------------- /validator/main.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "encoding/json" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/go-faster/errors" 11 | "github.com/google/uuid" 12 | "github.com/nextdotid/proof_server/config" 13 | "github.com/nextdotid/proof_server/headless" 14 | "github.com/nextdotid/proof_server/types" 15 | "github.com/samber/lo" 16 | ) 17 | 18 | var ( 19 | // PlatformFactories contains all supported platform factory. 20 | PlatformFactories map[types.Platform]func(*Base) IValidator 21 | ) 22 | 23 | type IValidator interface { 24 | // GeneratePostPayload gives a post structure (with 25 | // placeholders) for user to post on target platform. 26 | GeneratePostPayload() (post map[string]string) 27 | // GenerateSignPayload generates a string to be signed. 28 | GenerateSignPayload() (payload string) 29 | // Validate validates the proof. 30 | Validate() (err error) 31 | // GetAltID returns the altID of the proof. 32 | GetAltID() (altID string) 33 | } 34 | 35 | type Base struct { 36 | Platform types.Platform 37 | Previous string 38 | Action types.Action 39 | Pubkey *ecdsa.PublicKey 40 | // Identity on target platform. 41 | Identity string 42 | AltID string 43 | ProofLocation string 44 | Signature []byte 45 | SignaturePayload string 46 | Text string 47 | // Extra info needed by separate platforms (e.g. Ethereum) 48 | Extra map[string]string 49 | // CreatedAt indicates creation time of this link 50 | CreatedAt time.Time 51 | // Uuid gives this link an unique identifier, to let other 52 | // third-party service distinguish / store / dedup links with 53 | // ease. 54 | Uuid uuid.UUID 55 | } 56 | 57 | // BaseToInterface converts a `validator.Base` struct to 58 | // `validator.IValidator` interface. 59 | func BaseToInterface(v *Base) IValidator { 60 | performer_factory, ok := PlatformFactories[v.Platform] 61 | if !ok { 62 | return nil 63 | } 64 | 65 | return performer_factory(v) 66 | } 67 | 68 | func GetPostWithHeadlessBrowser(url string, regexp string) (post string, err error) { 69 | headlessEntrypoint := lo.Sample(config.C.Headless.Urls) 70 | headlessEntrypoint += "/v1/find" 71 | request := headless.FindRequest{ 72 | Location: url, 73 | Timeout: "5s", 74 | Match: headless.Match{ 75 | Type: "regexp", 76 | MatchRegExp: &headless.MatchRegExp{ 77 | Selector: "*", 78 | Value: regexp, 79 | }, 80 | MatchXPath: nil, 81 | MatchJS: nil, 82 | }, 83 | WaitXHR: false, 84 | } 85 | // POST request body to entrypoint headless server 86 | requestBody, err := json.Marshal(request) 87 | if err != nil { 88 | return "", err 89 | } 90 | respRaw, err := http.Post(headlessEntrypoint, "application/json", bytes.NewBuffer(requestBody)) 91 | if err != nil { 92 | return "", err 93 | } 94 | defer respRaw.Body.Close() 95 | response := headless.FindRespond{} 96 | err = json.NewDecoder(respRaw.Body).Decode(&response) 97 | if err != nil { 98 | return "", err 99 | } 100 | if response.Message != "" { 101 | return "", errors.Errorf("Error when fetching post from headless browser: %s", response.Message) 102 | } 103 | 104 | return response.Content, nil 105 | } 106 | 107 | // H for JSON builder. 108 | type H map[string]any 109 | -------------------------------------------------------------------------------- /validator/dns/dns_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func build() DNS { 16 | pk, _ := crypto.StringToSecp256k1Pubkey("0x028568e07ebf497b07a30f8a9d1731980736a4fac9d7c9c9b5682cb82dd3e774d7") 17 | createdAt, _ := util.TimestampStringToTime("1664267795") 18 | return DNS{ 19 | Base: &validator.Base{ 20 | Platform: types.Platforms.DNS, 21 | Previous: "", 22 | Action: types.Actions.Create, 23 | Pubkey: pk, 24 | Identity: "testcase.nextnext.id", 25 | CreatedAt: createdAt, 26 | Uuid: uuid.MustParse("80c98711-f4f6-43c7-b05c-8d86372f6131"), 27 | }, 28 | } 29 | } 30 | 31 | func build_invalid() DNS { 32 | dns := build() 33 | dns.Identity = "testcase_invalid.nextnext.id" 34 | return dns 35 | } 36 | 37 | func Test_query(t *testing.T) { 38 | t.Run("not found", func(t *testing.T) { 39 | _, err := query("nonexist.example.com") 40 | t.Log(err.Error()) 41 | require.Error(t, err) 42 | }) 43 | 44 | t.Run("found", func(t *testing.T) { 45 | body, err := query("example.com") 46 | require.NoError(t, err) 47 | require.NotNil(t, body) 48 | require.NotNil(t, body.Answer) 49 | }) 50 | } 51 | 52 | func Test_parseTxt(t *testing.T) { 53 | t.Run("success", func(t *testing.T) { 54 | txt := "ps:true;v:1;sig:Oyist/0E0MJ5sN3TI33P4EMBGTaCk2S3IQKzYfI5zxpwE2VdHClgLXfmj0L2dPydF8KOXyjbWWuM2AHKdW2DnwE=;ca:1664263102;uuid:26de5ec2-889e-4f59-9fac-dfa8d99e7ce7;prev:null" 55 | result, err := parseTxt(txt) 56 | require.NoError(t, err) 57 | require.Equal(t, "Oyist/0E0MJ5sN3TI33P4EMBGTaCk2S3IQKzYfI5zxpwE2VdHClgLXfmj0L2dPydF8KOXyjbWWuM2AHKdW2DnwE=", result.Signature) 58 | require.Nil(t, result.Previous) 59 | }) 60 | 61 | t.Run("struct field missing", func(t *testing.T) { 62 | txt := "ps:true;v:1;sig:Oyist/0E0MJ5sN3TI33P4EMBGTaCk2S3IQKzYfI5zxpwE2VdHClgLXfmj0L2dPydF8KOXyjbWWuM2AHKdW2DnwE=;ca:1664263102;uuid:26de5ec2-889e-4f59-9fac-dfa8d99e7ce7" 63 | _, err := parseTxt(txt) 64 | require.Error(t, err) 65 | }) 66 | } 67 | 68 | func Test_GeneratePostPayload(t *testing.T) { 69 | t.Run("success", func(t *testing.T) { 70 | dns := build() 71 | payload_map := dns.GeneratePostPayload() 72 | payload := payload_map["default"] 73 | require.Contains(t, payload, dns.Uuid.String()) 74 | require.Contains(t, payload, strconv.FormatInt(dns.CreatedAt.Unix(), 10)) 75 | require.Contains(t, payload, "%SIG_BASE64%") 76 | require.Contains(t, payload, "prev:null") 77 | }) 78 | } 79 | 80 | func Test_GenerateSignPayload(t *testing.T) { 81 | t.Run("success", func(t *testing.T) { 82 | dns := build() 83 | sp := dns.GenerateSignPayload() 84 | require.Contains(t, sp, types.Platforms.DNS) 85 | require.Contains(t, sp, types.Actions.Create) 86 | require.Contains(t, sp, dns.Uuid.String()) 87 | }) 88 | } 89 | 90 | func Test_Validate(t *testing.T) { 91 | t.Run("success", func(t *testing.T) { 92 | dns := build() 93 | require.NoError(t, dns.Validate()) 94 | require.Equal(t, dns.Identity, dns.AltID) 95 | }) 96 | 97 | t.Run("invalid", func(t *testing.T) { 98 | dns := build_invalid() 99 | err := dns.Validate() 100 | require.Error(t, err) 101 | t.Log(err.Error()) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /validator/tiktok/tiktok.go: -------------------------------------------------------------------------------- 1 | package tiktok 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/nextdotid/proof_server/types" 11 | "github.com/nextdotid/proof_server/util" 12 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 13 | "github.com/nextdotid/proof_server/validator" 14 | "github.com/sirupsen/logrus" 15 | "golang.org/x/xerrors" 16 | ) 17 | 18 | type TikTok struct { 19 | *validator.Base 20 | } 21 | 22 | const ( 23 | MATCH_TEMPLATE = `Sig: (.+?)[\s\n;]` 24 | MATCH_MISC = `\bMisc: ([^|]+)\|(\d+)\|(.+)?$` 25 | ) 26 | 27 | var ( 28 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "tiktok"}) 29 | POST_STRUCT = map[string]string{ 30 | "default": `🎭 NextID ROCKS! Sig: %%SIG_BASE64%%;Misc: %s|%s|%s`, 31 | } 32 | re = regexp.MustCompile(MATCH_TEMPLATE) 33 | ) 34 | 35 | func Init() { 36 | if validator.PlatformFactories == nil { 37 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 38 | } 39 | validator.PlatformFactories[types.Platforms.TikTok] = func(base *validator.Base) validator.IValidator { 40 | tt := TikTok{base} 41 | return &tt 42 | } 43 | } 44 | 45 | func (tt *TikTok) GeneratePostPayload() (post map[string]string) { 46 | post = make(map[string]string, 0) 47 | for lang_code, template := range POST_STRUCT { 48 | post[lang_code] = fmt.Sprintf(template, tt.Identity, tt.Uuid.String(), util.TimeToTimestampString(tt.CreatedAt), tt.Previous) 49 | } 50 | 51 | return post 52 | } 53 | 54 | func (tt *TikTok) GenerateSignPayload() (payload string) { 55 | tt.Identity = strings.ToLower(tt.Identity) 56 | payloadStruct := validator.H{ 57 | "action": string(tt.Action), 58 | "identity": tt.Identity, 59 | "platform": types.Platforms.TikTok, 60 | "prev": nil, 61 | "created_at": util.TimeToTimestampString(tt.CreatedAt), 62 | "uuid": tt.Uuid.String(), 63 | } 64 | if tt.Previous != "" { 65 | payloadStruct["prev"] = tt.Previous 66 | } 67 | 68 | payloadBytes, err := json.Marshal(payloadStruct) 69 | if err != nil { 70 | l.Warnf("Error when marshaling struct: %s", err.Error()) 71 | return "" 72 | } 73 | 74 | return string(payloadBytes) 75 | } 76 | 77 | func (tt *TikTok) Validate() (err error) { 78 | oembedInfo, err := fetchOembedInfo(tt.ProofLocation) 79 | if err != nil { 80 | return xerrors.Errorf("error when fetching tiktok proof: %w", err) 81 | } 82 | if tt.Identity != oembedInfo.AuthorUniqueID { 83 | return xerrors.Errorf("tiktok user mismatch: %s instead of %s", oembedInfo.AuthorUniqueID, tt.Identity) 84 | } 85 | signature, err := extractSignatureFromTitle(oembedInfo.Title) 86 | if err != nil { 87 | return err 88 | } 89 | tt.Signature = signature 90 | tt.ProofLocation = oembedInfo.EmbedProductID 91 | return mycrypto.ValidatePersonalSignature(tt.GenerateSignPayload(), tt.Signature, tt.Pubkey) 92 | } 93 | 94 | func (tt *TikTok) GetAltID() string { 95 | return "" 96 | } 97 | 98 | func extractSignatureFromTitle(title string) (signature []byte, err error) { 99 | result := re.FindStringSubmatch(title) 100 | if len(result) != 2 { 101 | return []byte{}, xerrors.New("signature not found in tiktok title") 102 | } 103 | signature, err = base64.StdEncoding.DecodeString(result[1]) 104 | if err != nil { 105 | return []byte{}, xerrors.Errorf("when decoding tiktok signature: %w", err) 106 | } 107 | 108 | return signature, nil 109 | } 110 | -------------------------------------------------------------------------------- /validator/telegram/telegram_test.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/config" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func before_each(t *testing.T) { 17 | logrus.SetLevel(logrus.DebugLevel) 18 | config.Init("../../config/config.test.json") 19 | } 20 | 21 | func generate() Telegram { 22 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x04666b700aeb6a6429f13cbb263e1bc566cd975a118b61bc796204109c1b351d19b7df23cc47f004e10fef41df82bad646b027578f8881f5f1d2f70c80dfcd8031") 23 | created_at, _ := util.TimestampStringToTime("1647503071") 24 | return Telegram{ 25 | Base: &validator.Base{ 26 | Platform: types.Platforms.Telegram, 27 | Previous: "", 28 | Action: types.Actions.Create, 29 | Pubkey: pubkey, 30 | Identity: "yeiwb", 31 | ProofLocation: "1504363098328924163", 32 | Text: "", 33 | Uuid: uuid.MustParse("c6fa1483-1bad-4f07-b661-678b191ab4b3"), 34 | CreatedAt: created_at, 35 | }, 36 | } 37 | } 38 | 39 | func generateBase1024Encode() Telegram { 40 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x04d7c5e01bedf1c993f40ec302d9bf162620daea93a7155cd9a8019ae3a2c2a476873e66c7ab9c5dbf9a6bd24ef4432298e70c5c7e7b148a54724a1d7b59e06bd8") 41 | created_at, _ := util.TimestampStringToTime("1650883741") 42 | return Telegram{ 43 | Base: &validator.Base{ 44 | Platform: types.Platforms.Telegram, 45 | Previous: "", 46 | Action: types.Actions.Create, 47 | Pubkey: pubkey, 48 | Identity: "SannieInMeta", 49 | ProofLocation: "1518542666987819009", 50 | Text: "", 51 | Uuid: uuid.MustParse("223a5c86-540b-49b7-8674-94e04a390cd0"), 52 | CreatedAt: created_at, 53 | }, 54 | } 55 | } 56 | 57 | func Test_GeneratePostPayload(t *testing.T) { 58 | t.Run("success", func(t *testing.T) { 59 | before_each(t) 60 | message := generate() 61 | result := message.GeneratePostPayload() 62 | require.Contains(t, result["default"], "Verifying my Telegram ID") 63 | require.Contains(t, result["default"], message.Identity) 64 | require.Contains(t, result["default"], "%SIG_BASE64%") 65 | }) 66 | } 67 | 68 | func Test_Validate(t *testing.T) { 69 | t.Run("success", func(t *testing.T) { 70 | before_each(t) 71 | 72 | message := generate() 73 | require.Nil(t, message.Validate()) 74 | require.Greater(t, len(message.Text), 10) 75 | require.NotEmpty(t, message.Text) 76 | require.Equal(t, "yeiwb", message.Identity) 77 | require.Equal(t, "1468853291941773312", message.AltID) 78 | }) 79 | 80 | t.Run("success on encode base1024", func(t *testing.T) { 81 | before_each(t) 82 | message := generateBase1024Encode() 83 | require.Nil(t, message.Validate()) 84 | require.Greater(t, len(message.Text), 10) 85 | require.NotEmpty(t, message.Text) 86 | require.Equal(t, "sannieinmeta", message.Identity) 87 | }) 88 | 89 | t.Run("should return identity error", func(t *testing.T) { 90 | before_each(t) 91 | 92 | message := generate() 93 | message.Identity = "foobar" 94 | require.NotNil(t, message.Validate()) 95 | }) 96 | 97 | t.Run("should return proof location not found", func(t *testing.T) { 98 | before_each(t) 99 | message := generate() 100 | message.ProofLocation = "123456" 101 | require.NotNil(t, message.Validate()) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /config/main.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type Config struct { 13 | DB DBConfig `json:"db"` 14 | Headless HeadlessConfig `json:"headless"` 15 | Platform PlatformConfig `json:"platform"` 16 | Arweave ArweaveConfig `json:"arweave"` 17 | Sqs SqsConfig `json:"sqs"` 18 | } 19 | 20 | type DBConfig struct { 21 | Host string `json:"host"` 22 | ReadOnlyHosts []string `json:"read_only_hosts"` 23 | Port uint `json:"port"` 24 | User string `json:"user"` 25 | Password string `json:"password"` 26 | DBName string `json:"db_name"` 27 | TZ string `json:"tz"` 28 | } 29 | 30 | type HeadlessConfig struct { 31 | Urls []string `json:"urls"` 32 | } 33 | 34 | type PlatformConfig struct { 35 | Twitter TwitterPlatformConfig `json:"twitter"` 36 | Telegram TelegramPlatformConfig `json:"telegram"` 37 | Ethereum EthereumPlatformConfig `json:"ethereum"` 38 | Discord DiscordPlatformConfig `json:"discord"` 39 | Slack SlackPlatformConfig `json:"slack"` 40 | } 41 | 42 | type TwitterPlatformConfig struct { 43 | // Twitter API v2 Bearer token 44 | OauthToken string `json:"oauth_token"` 45 | } 46 | 47 | type ArweaveConfig struct { 48 | Jwk string `json:"jwk"` 49 | ClientUrl string `json:"client_url"` 50 | } 51 | 52 | type SqsConfig struct { 53 | QueueName string `json:"queue_name"` 54 | } 55 | 56 | type TelegramPlatformConfig struct { 57 | ApiID int `json:"api_id"` 58 | ApiHash string `json:"api_hash"` 59 | BotToken string `json:"bot_token"` 60 | PublicChannelName string `json:"public_channel_name"` 61 | } 62 | 63 | type SlackPlatformConfig struct { 64 | ApiToken string `json:"api_token"` 65 | PublicChannelID string `json:"public_channel_id"` 66 | } 67 | 68 | type EthereumPlatformConfig struct { 69 | RPCServer string `json:"rpc_server"` 70 | } 71 | 72 | type CliConfig struct { 73 | ServerURL string `json:"server_url"` 74 | UploadPath string `json:"upload_url"` 75 | QueryPath string `json:"query_url"` 76 | } 77 | 78 | type DiscordPlatformConfig struct { 79 | BotToken string `json:"bot_token"` 80 | ProofServerChannelID string `json:"proof_server_channel_id"` 81 | } 82 | 83 | var ( 84 | C *Config = new(Config) 85 | Viper *viper.Viper 86 | ) 87 | 88 | func Init(configPath string) { 89 | if C.DB.Host != "" { // Initialized 90 | return 91 | } 92 | configContent, err := os.ReadFile(configPath) 93 | if err != nil { 94 | logrus.Fatalf("Error during opening config file! %v", err) 95 | } 96 | 97 | err = json.Unmarshal(configContent, C) 98 | if err != nil { 99 | logrus.Fatalf("Error duriong unmarshaling config file: %v", err) 100 | } 101 | } 102 | 103 | func InitCliConfig() { 104 | Viper = viper.New() 105 | 106 | Viper.SetConfigName("cli") // config file name without extension 107 | Viper.SetConfigType("toml") 108 | //viper.AddConfigPath(".") 109 | Viper.AddConfigPath("./config/") // config file path 110 | //viper.AutomaticEnv() // read value ENV variable 111 | 112 | err := Viper.ReadInConfig() 113 | if err != nil { 114 | fmt.Printf("fatal error config file: cli err:%v \n", err) 115 | os.Exit(1) 116 | } 117 | } 118 | 119 | func GetDatabaseDSN(host string) string { 120 | template := "host=%s port=%d user=%s password=%s dbname=%s TimeZone=%s sslmode=disable" 121 | return fmt.Sprintf(template, 122 | host, 123 | C.DB.Port, 124 | C.DB.User, 125 | C.DB.Password, 126 | C.DB.DBName, 127 | C.DB.TZ, 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /controller/subkey_query.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nextdotid/proof_server/model" 6 | "github.com/nextdotid/proof_server/types" 7 | "github.com/nextdotid/proof_server/util/crypto" 8 | "github.com/samber/lo" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | type subkeyQueryRequest struct { 13 | Avatar string `form:"avatar"` 14 | PublicKey string `form:"public_key"` 15 | Algorithm string `form:"algorithm"` 16 | } 17 | 18 | type subkeyQueryResponse struct { 19 | Subkeys []subkeyQueryResponseSingle `json:"subkeys"` 20 | } 21 | 22 | type subkeyQueryResponseSingle struct { 23 | Avatar string `json:"avatar"` 24 | Algorithm types.SubkeyAlgorithm `json:"algorithm"` 25 | PublicKey string `json:"public_key"` 26 | Name string `json:"name"` 27 | RP_ID string `json:"rp_id"` 28 | CreatedAt int64 `json:"created_at"` 29 | } 30 | 31 | // GET /v1/subkey 32 | func subkeyQuery(c *gin.Context) { 33 | req := subkeyQueryRequest{} 34 | var err error 35 | if err = c.BindQuery(&req); err != nil { 36 | errorResp(c, 400, err) 37 | return 38 | } 39 | if err = subkeyQueryRequestValid(&req); err != nil { 40 | errorResp(c, 400, err) 41 | return 42 | } 43 | 44 | var subkeys []model.Subkey 45 | if req.Avatar != "" { 46 | subkeys, err = subkeyQueryAvatar(&req) 47 | } else { 48 | subkeys, err = subkeyQuerySubkey(&req) 49 | } 50 | if err != nil { 51 | errorResp(c, 500, err) 52 | return 53 | } 54 | response := subkeyQueryResponse{ 55 | Subkeys: lo.Map(subkeys, func(s model.Subkey, index int) subkeyQueryResponseSingle { 56 | return subkeyQueryResponseSingle{ 57 | Avatar: s.Avatar, 58 | Algorithm: s.Algorithm, 59 | PublicKey: s.PublicKey, 60 | Name: s.Name, 61 | RP_ID: s.RP_ID, 62 | CreatedAt: s.CreatedAt.Unix(), 63 | } 64 | }), 65 | } 66 | c.JSON(200, response) 67 | } 68 | 69 | func subkeyQueryAvatar(req *subkeyQueryRequest) (subkeys []model.Subkey, err error) { 70 | subkeys = make([]model.Subkey, 0) 71 | tx := model.ReadOnlyDB.Model(&model.Subkey{}) 72 | result := tx.Where("avatar", req.Avatar).Find(&subkeys) 73 | if result.Error != nil { 74 | return []model.Subkey{}, result.Error 75 | } 76 | return subkeys, nil 77 | } 78 | 79 | func subkeyQuerySubkey(req *subkeyQueryRequest) (subkeys []model.Subkey, err error) { 80 | subkeys = make([]model.Subkey, 0) 81 | tx := model.ReadOnlyDB.Model(&model.Subkey{}) 82 | result := tx.Where("algorithm", req.Algorithm).Where("public_key", req.PublicKey).Find(&subkeys) 83 | if result.Error != nil { 84 | return []model.Subkey{}, result.Error 85 | } 86 | return subkeys, nil 87 | } 88 | 89 | func subkeyQueryRequestValid(req *subkeyQueryRequest) error { 90 | if req.Avatar == "" && req.PublicKey == "" { 91 | return xerrors.New("Avatar or public_key should be given") 92 | } 93 | if req.Avatar != "" { 94 | _, err := crypto.StringToSecp256k1Pubkey(req.Avatar) 95 | if err != nil { 96 | return xerrors.Errorf("Error when parsing avatar: %w", err) 97 | } 98 | } else { 99 | switch req.Algorithm { 100 | case string(types.SubkeyAlgorithms.Secp256K1): 101 | _, err := crypto.StringToSecp256k1Pubkey(req.PublicKey) 102 | if err != nil { 103 | return xerrors.Errorf("Error when parsing subkey: %w", err) 104 | } 105 | case string(types.SubkeyAlgorithms.Secp256R1): 106 | _, err := crypto.StringToSecp256r1Pubkey(req.PublicKey) 107 | if err != nil { 108 | return xerrors.Errorf("Error when parsing subkey: %w", err) 109 | } 110 | default: 111 | return xerrors.New("One of avatar or algorighm should be given.") 112 | } 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /validator/twitter/api.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | twitter "github.com/g8rswimmer/go-twitter/v2" 10 | "github.com/nextdotid/proof_server/config" 11 | "golang.org/x/xerrors" 12 | ) 13 | 14 | type APIResponse struct { 15 | User struct { 16 | ID string `json:"user_id"` 17 | ScreenName string `json:"screen_name"` 18 | } `json:"user"` 19 | Text string `json:"text"` 20 | } 21 | 22 | var ( 23 | twitterClient *twitter.Client 24 | ) 25 | 26 | type authorize struct { 27 | Token string 28 | } 29 | 30 | func (a authorize) Add(req *http.Request) { 31 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Token)) 32 | } 33 | 34 | func initTwitterClient() { 35 | if twitterClient != nil { 36 | return 37 | } 38 | twitterClient = &twitter.Client{ 39 | Authorizer: authorize{ 40 | Token: config.C.Platform.Twitter.OauthToken, 41 | }, 42 | Client: http.DefaultClient, 43 | Host: "https://api.twitter.com", 44 | } 45 | } 46 | 47 | // Fetch tweet using twitter OAuth2.0 API. 48 | // FIXME: should be switched to guest OAuth token solution. 49 | func fetchPostWithAPI(id string, maxRetries int) (*APIResponse, error) { 50 | initTwitterClient() 51 | opts := twitter.TweetLookupOpts{ 52 | Expansions: []twitter.Expansion{twitter.ExpansionEntitiesMentionsUserName, twitter.ExpansionAuthorID}, 53 | TweetFields: []twitter.TweetField{twitter.TweetFieldText, twitter.TweetFieldCreatedAt, twitter.TweetFieldEntities}, 54 | } 55 | result, err := twitterClient.TweetLookup(context.Background(), []string{id}, opts) 56 | if err != nil { 57 | return nil, xerrors.Errorf("error when retriving tweet: %w", err) 58 | } 59 | tweet := result.Raw.Tweets[0] 60 | if tweet == nil { 61 | return nil, xerrors.Errorf("tweet not found: %s", id) 62 | } 63 | 64 | response := APIResponse{ 65 | Text: tweet.Text, 66 | } 67 | response.User.ID = tweet.AuthorID 68 | userName, err := fetchUserName(tweet.AuthorID) 69 | if err != nil { 70 | return nil, err 71 | } 72 | response.User.ScreenName = userName 73 | 74 | return &response, nil 75 | } 76 | 77 | func fetchUserName(userID string) (userName string, err error) { 78 | initTwitterClient() 79 | opts := twitter.UserLookupOpts{ 80 | UserFields: []twitter.UserField{twitter.UserFieldUserName}, 81 | } 82 | result, err := twitterClient.UserLookup(context.Background(), []string{userID}, opts) 83 | if err != nil { 84 | return "", xerrors.Errorf("error when fetching twitter username: %w", err) 85 | } 86 | users := result.Raw.UserDictionaries() 87 | user, ok := users[userID] 88 | if !ok { 89 | return "", xerrors.Errorf("error when fetching twitter username: user not found for ID %s", userID) 90 | } 91 | return strings.ToLower(user.User.UserName), nil 92 | } 93 | 94 | // func fetchPostWithAPI(id string, maxRetries int) (tweet *APIResponse, err error) { 95 | // const RETRY_AFTER = time.Second 96 | // ctx := context.Background() 97 | // if CurrentTokenList == nil { 98 | // CurrentTokenList, err = GetTokenListFromS3(ctx) 99 | // if err != nil { 100 | // return nil, xerrors.Errorf("fetchPostWithAPI: %w", err) 101 | // } 102 | // if CurrentTokenList == nil { 103 | // return nil, xerrors.Errorf("twitter token list does not exist") 104 | // } 105 | // } 106 | // token := lo.Sample(CurrentTokenList.Tokens) 107 | // if lo.IsEmpty(token.OAuthSecret) || lo.IsEmpty(token.OAuthKey) { 108 | // return nil, xerrors.Errorf("twitter token seems to be empty") 109 | // } 110 | 111 | // // TODO: Use token to query specific tweet with twitter API 112 | // // https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets 113 | // // https://api.twitter.com/1.1/statuses/show.json 114 | 115 | // return nil, nil 116 | // } 117 | -------------------------------------------------------------------------------- /docs/proof_chain.md: -------------------------------------------------------------------------------- 1 | # Structure of Proof Chain 2 | 3 | ## Type declaration 4 | 5 | ```typescript 6 | const VERSION = "1"; 7 | 8 | // assert(signature.match(/0x[a-f0-9]{144}/)) 9 | // Sample: 10 | // 0x3046022100881328457aa312135c37e1ddf8a129717274ce3f389c176936f5cb44edf04fc4022100be183139154d108ce2e5d6ba16678b0dbeb3b7d70caac2b00b2dad8f81e87790 11 | type Signature = string; 12 | 13 | // assert(public_key.match(/^0x[a-f0-9]{130}$/)) 14 | // Sample: 15 | // 0x0428b73a2b67a88a47edb15bed5c73a199e24287bb12997c54239e9e6815e24a3032a502d58afe3f36a54f2f7606022907f358d0dd58939cffa0a845c5043ce038 16 | type PublicKey = string; 17 | 18 | // All available chain modification actions 19 | enum Action { 20 | Create = "create", 21 | Delete = "delete", 22 | } 23 | 24 | // All supported platforms 25 | enum Platform { 26 | Twitter = "twitter", 27 | Keybase = "keybase", 28 | } 29 | 30 | // Every link in the proof chain 31 | interface Link { 32 | // If this is genesis link, leave it null; else, it equals 33 | // previous link's signature. Worked as a pointer. 34 | prev: Signature | null; 35 | action: Action; 36 | platform: Platform; 37 | identity: string; 38 | // if method === Method.Add, then it must be a string; else, left null 39 | proof_location: string | null; 40 | // UNIX timestamp (unit: second) 41 | created_at: number; 42 | // Signature of self 43 | signature: Signature; 44 | } 45 | 46 | // Main struct 47 | interface Chain { 48 | version: VERSION; 49 | avatar: { 50 | public_key: PublicKey, 51 | curve: "secp256k1", 52 | }; 53 | links: Link[]; 54 | } 55 | ``` 56 | 57 | 58 | ## Example 59 | 60 | ```javascript 61 | { 62 | "version": "1", 63 | "avatar": { 64 | "public_key": "0x0485554db28de6fefb7fe532164b67372a5e9d78dfd7f77e09a8b274f777c3e64f2e20353df005a83dbe4c5ca663638621ce4d1dd0c9586ab3fc71286b74741ed8", 65 | "curve": "secp256k1" 66 | }, 67 | "links": [{ 68 | "prev": null, // Genesis 69 | "action": "create", 70 | "platform": "twitter", 71 | "identity": "twitter_screen_name", 72 | "proof_location": "https://twitter.com/twitter_screen_name/11111111111111", 73 | "created_at": 1638618231, 74 | "signature": "0xSIG1" 75 | }, { 76 | "prev": "0xSIG1", 77 | "action": "create", 78 | "platform": "keybase", 79 | "identity": "keybase_username", 80 | "proof_location": "https://keybase_username.keybase.pub/NextID/proof.txt", 81 | "created_at": 1638618470, 82 | "signature": "0xSIG2" 83 | }] 84 | } 85 | ``` 86 | 87 | ## How to sign 88 | 89 | ```typescript 90 | // Pseudo-code for how to sign a link 91 | function sign_link(link: Link): Signature { 92 | // Omit "signature" and "proof_location" KV from original link 93 | let signature_payload_struct = Object.assign({}, link); 94 | delete(signature_payload_struct.signature); 95 | delete(signature_payload_struct.proof_location); 96 | 97 | // Sort this object by key 98 | const signature_payload_struct_sorted = sort_key(signature_payload_struct); 99 | 100 | // JSONify 101 | const signature_payload = JSON.stringify(signature_payload_struct_sorted); 102 | 103 | // Sign this using web3 personal_sign method 104 | // Specifically: 105 | let personal_signature_payload = keccak256("\x19Ethereum Signed Message:\n" + signature_payload.length + signature_payload) 106 | let signature_bin: Buffer = avatar_private_key.sign(personal_signature_payload) 107 | let signature = "0x" + Base16.encode(signature_bin, {case: 'lower'}) 108 | 109 | // const signature = web3.eth.personal.sign(signature_payload, avatar_private_key); 110 | 111 | // Final artifact should be a format like below: 112 | assert(signature.match(/^0x[0-9a-f]{130}$/)) 113 | return signature; 114 | } 115 | ``` 116 | -------------------------------------------------------------------------------- /cmd/lambda/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/akrylysov/algnhsa" 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 12 | "github.com/nextdotid/proof_server/common" 13 | myconfig "github.com/nextdotid/proof_server/config" 14 | "github.com/nextdotid/proof_server/controller" 15 | "github.com/nextdotid/proof_server/model" 16 | "github.com/nextdotid/proof_server/util/sqs" 17 | "github.com/nextdotid/proof_server/validator/activitypub" 18 | "github.com/nextdotid/proof_server/validator/das" 19 | "github.com/nextdotid/proof_server/validator/discord" 20 | "github.com/nextdotid/proof_server/validator/dns" 21 | "github.com/nextdotid/proof_server/validator/ethereum" 22 | "github.com/nextdotid/proof_server/validator/github" 23 | "github.com/nextdotid/proof_server/validator/keybase" 24 | "github.com/nextdotid/proof_server/validator/minds" 25 | "github.com/nextdotid/proof_server/validator/solana" 26 | "github.com/nextdotid/proof_server/validator/steam" 27 | "github.com/nextdotid/proof_server/validator/twitter" 28 | "github.com/sirupsen/logrus" 29 | ) 30 | 31 | var ( 32 | initialized = false 33 | ) 34 | 35 | func init_db(cfg aws.Config) { 36 | shouldMigrate := getE("DB_MIGRATE", "false") 37 | model.Init(shouldMigrate == "true") 38 | } 39 | 40 | func init_sqs(cfg aws.Config) { 41 | sqs.Init(cfg) 42 | } 43 | 44 | func init_validators() { 45 | twitter.Init() 46 | ethereum.Init() 47 | keybase.Init() 48 | github.Init() 49 | discord.Init() 50 | das.Init() 51 | solana.Init() 52 | minds.Init() 53 | dns.Init() 54 | steam.Init() 55 | activitypub.Init() 56 | } 57 | 58 | func init() { 59 | cfg, err := config.LoadDefaultConfig( 60 | context.Background(), 61 | // TODO: change region 62 | config.WithRegion("ap-east-1"), 63 | ) 64 | if err != nil { 65 | logrus.Fatalf("Unable to load AWS config: %s", err) 66 | } 67 | init_config_from_aws_secret() 68 | logrus.SetLevel(logrus.InfoLevel) 69 | common.CurrentRuntime = common.Runtimes.Lambda 70 | 71 | init_db(cfg) 72 | init_sqs(cfg) 73 | init_validators() 74 | controller.Init() 75 | } 76 | 77 | func main() { 78 | algnhsa.ListenAndServe(controller.Engine, nil) 79 | } 80 | 81 | func init_config_from_aws_secret() { 82 | if initialized { 83 | return 84 | } 85 | secret_name := getE("SECRET_NAME", "") 86 | region := getE("SECRET_REGION", "") 87 | 88 | // Create a Secrets Manager client 89 | cfg, err := config.LoadDefaultConfig( 90 | context.Background(), 91 | config.WithRegion(region), 92 | ) 93 | if err != nil { 94 | logrus.Fatalf("Unable to load SDK config: %v", err) 95 | } 96 | 97 | client := secretsmanager.NewFromConfig(cfg) 98 | input := secretsmanager.GetSecretValueInput{ 99 | SecretId: aws.String(secret_name), 100 | VersionStage: aws.String("AWSCURRENT"), 101 | } 102 | result, err := client.GetSecretValue(context.Background(), &input) 103 | if err != nil { 104 | logrus.Fatalf("Error occured: %s", err.Error()) 105 | } 106 | 107 | // Decrypts secret using the associated KMS CMK. 108 | // Depending on whether the secret is a string or binary, one of these fields will be populated. 109 | if result.SecretString == nil { 110 | logrus.Fatalf("cannot get secret string") 111 | } 112 | secret_string := *result.SecretString 113 | 114 | err = json.Unmarshal([]byte(secret_string), myconfig.C) 115 | if err != nil { 116 | logrus.Fatalf("Error during parsing config JSON: %v", err) 117 | } 118 | initialized = true 119 | } 120 | 121 | func getE(env_key, default_value string) string { 122 | result := os.Getenv(env_key) 123 | if len(result) == 0 { 124 | if len(default_value) > 0 { 125 | return default_value 126 | } else { 127 | logrus.Fatalf("ENV %s must be given! Abort.", env_key) 128 | return "" 129 | } 130 | 131 | } else { 132 | return result 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /util/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "fmt" 7 | "math/big" 8 | "strings" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/crypto" 12 | "golang.org/x/xerrors" 13 | ) 14 | 15 | // ValidatePersonalSignature checks whether (eth.personal.sign) signature, 16 | // payload and pubkey are matched. 17 | // Pubkey and signature should be without "0x". 18 | func ValidatePersonalSignature(payload string, signature []byte, pubkey *ecdsa.PublicKey) (err error) { 19 | pubkeyRecovered, err := RecoverPubkeyFromPersonalSignature(payload, signature) 20 | if err != nil { 21 | return xerrors.Errorf("%w", err) 22 | } 23 | 24 | if crypto.PubkeyToAddress(*pubkey) != crypto.PubkeyToAddress(*pubkeyRecovered) { 25 | return xerrors.Errorf("bad signature") 26 | } 27 | return nil 28 | } 29 | 30 | // RecoverPubkeyFromPersonalSignature extract a public key from signature 31 | func RecoverPubkeyFromPersonalSignature(payload string, signature []byte) (pubkey *ecdsa.PublicKey, err error) { 32 | // Recover pubkey from signature 33 | if len(signature) != 65 { 34 | return nil, xerrors.Errorf("Error: Signature length invalid: %d instead of 65", len(signature)) 35 | } 36 | if signature[64] == 27 || signature[64] == 28 { 37 | signature[64] -= 27 38 | } 39 | 40 | if signature[64] != 0 && signature[64] != 1 { 41 | return nil, xerrors.Errorf("Error: Signature Recovery ID not supported: %d", signature[64]) 42 | } 43 | 44 | pubkeyRecovered, err := crypto.SigToPub(signPersonalHash([]byte(payload)), signature) 45 | if err != nil { 46 | return nil, xerrors.Errorf("Error when recovering pubkey from signature: %s", err.Error()) 47 | } 48 | 49 | return pubkeyRecovered, nil 50 | } 51 | 52 | // GenerateSecp256k1Keypair generates a keypair. 53 | // For test purpose only. 54 | func GenerateSecp256k1Keypair() (publicKey *ecdsa.PublicKey, privateKey *ecdsa.PrivateKey) { 55 | privateKey, _ = crypto.GenerateKey() 56 | publicKey = &privateKey.PublicKey 57 | return publicKey, privateKey 58 | } 59 | 60 | // SignPersonal signs a payload using given secret key. 61 | // For test purpose only. 62 | func SignPersonal(payload []byte, sk *ecdsa.PrivateKey) (signature []byte, err error) { 63 | hash := signPersonalHash(payload) 64 | signature, err = crypto.Sign(hash, sk) 65 | if err != nil { 66 | return nil, xerrors.Errorf("%w", err) 67 | } 68 | 69 | return signature, nil 70 | } 71 | 72 | func signPersonalHash(data []byte) []byte { 73 | messsage := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) 74 | return crypto.Keccak256([]byte(messsage)) 75 | } 76 | 77 | // StringToSecp256k1Pubkey is compatible with comressed / uncompressed pubkey 78 | // hex, and with / without '0x' head. 79 | func StringToSecp256k1Pubkey(pkHex string) (*ecdsa.PublicKey, error) { 80 | pkBytes := common.Hex2Bytes(strings.ToLower(strings.TrimPrefix(pkHex, "0x"))) 81 | return BytesToSecp256k1PubKey(pkBytes) 82 | } 83 | 84 | // BytesToSecp256k1PubKey is compatible with comressed / uncompressed pubkey 85 | // bytes. 86 | func BytesToSecp256k1PubKey(pkBytes []byte) (result *ecdsa.PublicKey, err error) { 87 | if len(pkBytes) == 33 { // compressed 88 | result, err = crypto.DecompressPubkey(pkBytes) 89 | } else { 90 | result, err = crypto.UnmarshalPubkey(pkBytes) 91 | } 92 | return 93 | } 94 | 95 | // CompressedPubkeyHex has no "0x". 96 | func CompressedPubkeyHex(pk *ecdsa.PublicKey) string { 97 | return common.Bytes2Hex(crypto.CompressPubkey(pk)) 98 | } 99 | 100 | // StringToSecp256r1Pubkey is compatible with 101 | // `X_CONCAT_Y_64_BYTES_HEXSTRING` public key representation. 102 | func StringToSecp256r1Pubkey(pkHex string) (*ecdsa.PublicKey, error) { 103 | pkBinary := common.FromHex(strings.ToLower(strings.TrimPrefix(pkHex, "0x"))) 104 | if len(pkBinary) != 64 { // X:32 Y:32 105 | return nil, xerrors.Errorf("wrong public key length: expect 64, got %d", len(pkBinary)) 106 | } 107 | return &ecdsa.PublicKey{ 108 | Curve: elliptic.P256(), 109 | X: new(big.Int).SetBytes(pkBinary[:32]), 110 | Y: new(big.Int).SetBytes(pkBinary[32:]), 111 | }, nil 112 | } 113 | -------------------------------------------------------------------------------- /validator/twitter/twitter_test.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/config" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func before_each(t *testing.T) { 17 | logrus.SetLevel(logrus.DebugLevel) 18 | config.Init("../../config/config.test.json") 19 | } 20 | 21 | func generate() Twitter { 22 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x04666b700aeb6a6429f13cbb263e1bc566cd975a118b61bc796204109c1b351d19b7df23cc47f004e10fef41df82bad646b027578f8881f5f1d2f70c80dfcd8031") 23 | created_at, _ := util.TimestampStringToTime("1647503071") 24 | return Twitter{ 25 | Base: &validator.Base{ 26 | Platform: types.Platforms.Twitter, 27 | Previous: "", 28 | Action: types.Actions.Create, 29 | Pubkey: pubkey, 30 | Identity: "yeiwb", 31 | ProofLocation: "1504363098328924163", 32 | Text: "", 33 | Uuid: uuid.MustParse("c6fa1483-1bad-4f07-b661-678b191ab4b3"), 34 | CreatedAt: created_at, 35 | }, 36 | } 37 | } 38 | 39 | func generateBase1024Encode() Twitter { 40 | pubkey, _ := mycrypto.StringToSecp256k1Pubkey("0x04d7c5e01bedf1c993f40ec302d9bf162620daea93a7155cd9a8019ae3a2c2a476873e66c7ab9c5dbf9a6bd24ef4432298e70c5c7e7b148a54724a1d7b59e06bd8") 41 | created_at, _ := util.TimestampStringToTime("1650883741") 42 | return Twitter{ 43 | Base: &validator.Base{ 44 | Platform: types.Platforms.Twitter, 45 | Previous: "", 46 | Action: types.Actions.Create, 47 | Pubkey: pubkey, 48 | Identity: "SannieInMeta", 49 | ProofLocation: "1518542666987819009", 50 | Text: "", 51 | Uuid: uuid.MustParse("223a5c86-540b-49b7-8674-94e04a390cd0"), 52 | CreatedAt: created_at, 53 | }, 54 | } 55 | } 56 | 57 | func Test_GeneratePostPayload(t *testing.T) { 58 | t.Run("success", func(t *testing.T) { 59 | before_each(t) 60 | tweet := generate() 61 | result := tweet.GeneratePostPayload() 62 | require.Contains(t, result["default"], "Verifying my Twitter ID") 63 | require.Contains(t, result["default"], tweet.Identity) 64 | require.Contains(t, result["default"], "%SIG_BASE64%") 65 | }) 66 | } 67 | 68 | // FIXME: Have tested manually after headless browser is implemented. 69 | func Test_Validate(t *testing.T) { 70 | t.Run("success", func(t *testing.T) { 71 | before_each(t) 72 | 73 | tweet := generate() 74 | require.Nil(t, tweet.Validate()) 75 | require.Greater(t, len(tweet.Text), 10) 76 | require.NotEmpty(t, tweet.Text) 77 | require.Equal(t, "yeiwb", tweet.Identity) 78 | require.Equal(t, "1468853291941773312", tweet.AltID) 79 | }) 80 | 81 | t.Run("success on encode base1024", func(t *testing.T) { 82 | before_each(t) 83 | tweet := generateBase1024Encode() 84 | require.Nil(t, tweet.Validate()) 85 | require.Greater(t, len(tweet.Text), 10) 86 | require.NotEmpty(t, tweet.Text) 87 | require.Equal(t, "sannieinmeta", tweet.Identity) 88 | }) 89 | 90 | t.Run("should return identity error", func(t *testing.T) { 91 | before_each(t) 92 | 93 | tweet := generate() 94 | tweet.Identity = "foobar" 95 | require.NotNil(t, tweet.Validate()) 96 | }) 97 | 98 | t.Run("should return proof location not found", func(t *testing.T) { 99 | before_each(t) 100 | tweet := generate() 101 | tweet.ProofLocation = "123456" 102 | require.NotNil(t, tweet.Validate()) 103 | }) 104 | } 105 | 106 | func Test_MatchTemplateText(t *testing.T) { 107 | t.Run("success", func(t *testing.T) { 108 | text := " Verifying my Twitter ID @test123 for @NextDotID. Sig: s/c7gBZHbABYeFhhJfidrZ57EUgiTPO+PKiEM6mJoNBAvzcJ8R+YEAiTCQZzxEXaNiYQ5O6jvulp8Y0pBWsyfxw= Powered by Next.ID" 109 | matched := re.FindStringSubmatch(text) 110 | require.Equal(t, 2, len(matched)) 111 | require.Equal(t, "s/c7gBZHbABYeFhhJfidrZ57EUgiTPO+PKiEM6mJoNBAvzcJ8R+YEAiTCQZzxEXaNiYQ5O6jvulp8Y0pBWsyfxw=", matched[1]) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /validator/discord/discord.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "path" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/bwmarrin/discordgo" 13 | "github.com/nextdotid/proof_server/config" 14 | "github.com/nextdotid/proof_server/util" 15 | "golang.org/x/xerrors" 16 | 17 | "github.com/nextdotid/proof_server/types" 18 | "github.com/nextdotid/proof_server/util/crypto" 19 | "github.com/nextdotid/proof_server/validator" 20 | ) 21 | 22 | type Discord struct { 23 | *validator.Base 24 | } 25 | 26 | var ( 27 | re = regexp.MustCompile(MATCH_TEMPLATE) 28 | POST_TEMPLATE = map[string]string{ 29 | "default": "Verifying my discord ID: %s on NextID. \nSig: %%SIG_BASE64%%", 30 | "en-US": "Verifying my discord ID: %s on NextID. \nSig: %%SIG_BASE64%%", 31 | "zh-CN": "在NextID上认证我的账号: %s \nSig: %%SIG_BASE64%%", 32 | } 33 | ) 34 | 35 | const ( 36 | MATCH_TEMPLATE = "^Sig: (.*)$" 37 | ) 38 | 39 | func Init() { 40 | if validator.PlatformFactories == nil { 41 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 42 | } 43 | validator.PlatformFactories[types.Platforms.Discord] = func(base *validator.Base) validator.IValidator { 44 | dc := Discord{base} 45 | return &dc 46 | } 47 | } 48 | 49 | func (dc *Discord) GeneratePostPayload() (post map[string]string) { 50 | post = make(map[string]string, 0) 51 | for lang_code, template := range POST_TEMPLATE { 52 | post[lang_code] = fmt.Sprintf(template, dc.Identity) 53 | } 54 | return post 55 | } 56 | 57 | func (dc *Discord) GenerateSignPayload() (payload string) { 58 | payloadStruct := validator.H{ 59 | "action": string(dc.Action), 60 | "identity": dc.Identity, 61 | "platform": string(types.Platforms.Discord), 62 | "prev": nil, 63 | "created_at": util.TimeToTimestampString(dc.CreatedAt), 64 | "uuid": dc.Uuid.String(), 65 | } 66 | 67 | if dc.Previous != "" { 68 | payloadStruct["prev"] = dc.Previous 69 | } 70 | 71 | payload_bytes, _ := json.Marshal(payloadStruct) 72 | return string(payload_bytes) 73 | } 74 | 75 | func (dc *Discord) Validate() (err error) { 76 | dc.SignaturePayload = dc.GenerateSignPayload() 77 | 78 | // Delete. No need to fetch content from platform. 79 | if dc.Action == types.Actions.Delete { 80 | return crypto.ValidatePersonalSignature(dc.SignaturePayload, dc.Signature, dc.Pubkey) 81 | } 82 | 83 | u, err := url.Parse(dc.ProofLocation) 84 | urlPath := path.Clean(u.Path) 85 | pathArr := strings.Split(strings.TrimSpace(urlPath), "/") 86 | 87 | //proof location will be like: https://discord.com/channels/960708146706395176/960708146706395179/961458176719487076 88 | if len(pathArr) != 5 { 89 | return xerrors.Errorf("Error getting right proof location: %w", err) 90 | } 91 | 92 | client, err := discordgo.New("Bot " + config.C.Platform.Discord.BotToken) 93 | if err != nil { 94 | return xerrors.Errorf("Error creating Discord session: %w", err) 95 | } 96 | 97 | msgResp, err := client.ChannelMessage(pathArr[3], pathArr[4]) 98 | if err != nil { 99 | return xerrors.Errorf("Error getting the message from discord: %w", err) 100 | } 101 | 102 | if fmt.Sprintf("%s", msgResp.Author) != dc.Identity { 103 | return xerrors.Errorf("User name mismatch: expect %s - actual %s", dc.Identity, msgResp.Author) 104 | } 105 | 106 | dc.AltID = msgResp.Author.ID 107 | dc.Text = msgResp.Content 108 | return dc.validateText() 109 | } 110 | 111 | func (dc *Discord) GetAltID() string { 112 | return dc.AltID 113 | } 114 | 115 | func (dc *Discord) validateText() (err error) { 116 | scanner := bufio.NewScanner(strings.NewReader(dc.Text)) 117 | for scanner.Scan() { 118 | matched := re.FindStringSubmatch(scanner.Text()) 119 | if len(matched) < 2 { 120 | continue // Search for next line 121 | } 122 | sigBase64 := matched[1] 123 | sigBytes, err := util.DecodeString(sigBase64) 124 | if err != nil { 125 | return xerrors.Errorf("Error when decoding signature %s: %s", sigBase64, err.Error()) 126 | } 127 | dc.Signature = sigBytes 128 | return crypto.ValidatePersonalSignature(dc.SignaturePayload, sigBytes, dc.Pubkey) 129 | } 130 | return xerrors.Errorf("Signature not found in the message link.") 131 | } 132 | -------------------------------------------------------------------------------- /validator/ethereum/ethereum_test.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "encoding/base64" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ethereum/go-ethereum/crypto" 11 | "github.com/google/uuid" 12 | "github.com/nextdotid/proof_server/config" 13 | "github.com/nextdotid/proof_server/types" 14 | "github.com/nextdotid/proof_server/validator" 15 | 16 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 17 | "github.com/sirupsen/logrus" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | var ( 22 | persona_sk *ecdsa.PrivateKey 23 | wallet_sk *ecdsa.PrivateKey 24 | ) 25 | 26 | func before_each(t *testing.T) { 27 | logrus.SetLevel(logrus.DebugLevel) 28 | config.Init("../../config/config.test.json") 29 | } 30 | 31 | func generate() Ethereum { 32 | eth := Ethereum{ 33 | Base: &validator.Base{ 34 | Platform: types.Platforms.Ethereum, 35 | Previous: "", 36 | Action: types.Actions.Create, 37 | Extra: map[string]string{ 38 | "wallet_signature": "", 39 | }, 40 | CreatedAt: time.Now(), 41 | Uuid: uuid.New(), 42 | }, 43 | } 44 | _, persona_sk = mycrypto.GenerateSecp256k1Keypair() 45 | eth.Pubkey = &persona_sk.PublicKey 46 | 47 | _, wallet_sk = mycrypto.GenerateSecp256k1Keypair() 48 | eth.Identity = crypto.PubkeyToAddress(wallet_sk.PublicKey).Hex() 49 | 50 | // Generate sig 51 | eth.Signature, _ = mycrypto.SignPersonal([]byte(eth.GenerateSignPayload()), persona_sk) 52 | wallet_sig, _ := mycrypto.SignPersonal([]byte(eth.GenerateSignPayload()), wallet_sk) 53 | eth.Extra = map[string]string{ 54 | "wallet_signature": base64.StdEncoding.EncodeToString(wallet_sig), 55 | } 56 | 57 | return eth 58 | } 59 | 60 | func Test_GeneratePostPayload(t *testing.T) { 61 | t.Run("success", func(t *testing.T) { 62 | before_each(t) 63 | eth := generate() 64 | require.Equal(t, "", eth.GeneratePostPayload()["default"]) 65 | }) 66 | } 67 | 68 | func Test_GenerateSignPayload(t *testing.T) { 69 | t.Run("success", func(t *testing.T) { 70 | before_each(t) 71 | 72 | eth := generate() 73 | result := eth.GenerateSignPayload() 74 | require.Contains(t, result, "\"identity\":\""+strings.ToLower(crypto.PubkeyToAddress(wallet_sk.PublicKey).Hex())) 75 | require.Contains(t, result, "\"persona\":\"0x"+mycrypto.CompressedPubkeyHex(eth.Pubkey)) 76 | require.Contains(t, result, "\"platform\":\"ethereum\"") 77 | }) 78 | } 79 | 80 | func Test_Validate(t *testing.T) { 81 | t.Run("success", func(t *testing.T) { 82 | before_each(t) 83 | 84 | eth := generate() 85 | require.Nil(t, eth.Validate()) 86 | require.Equal(t, eth.AltID, eth.Identity) 87 | }) 88 | } 89 | 90 | func Test_Validate_Delete(t *testing.T) { 91 | t.Run("signed by persona", func(t *testing.T) { 92 | before_each(t) 93 | 94 | eth := generate() 95 | eth.Action = types.Actions.Delete 96 | eth.Extra = map[string]string{ 97 | "wallet_signature": "", 98 | } 99 | eth.Signature, _ = mycrypto.SignPersonal([]byte(eth.GenerateSignPayload()), persona_sk) 100 | 101 | require.Nil(t, eth.Validate()) 102 | }) 103 | 104 | t.Run("signed by wallet", func(t *testing.T) { 105 | before_each(t) 106 | 107 | eth := generate() 108 | eth.Action = types.Actions.Delete 109 | wallet_sig, _ := mycrypto.SignPersonal([]byte(eth.GenerateSignPayload()), wallet_sk) 110 | eth.Extra = map[string]string{ 111 | "wallet_signature": base64.StdEncoding.EncodeToString(wallet_sig), 112 | } 113 | 114 | require.Nil(t, eth.Validate()) 115 | }) 116 | 117 | t.Run("signed by persona, but put in wallet_signature", func(t *testing.T) { 118 | before_each(t) 119 | 120 | eth := generate() 121 | eth.Action = types.Actions.Delete 122 | 123 | eth.Signature, _ = mycrypto.SignPersonal([]byte(eth.GenerateSignPayload()), persona_sk) 124 | eth.Extra = map[string]string{ 125 | "wallet_signature": base64.StdEncoding.EncodeToString(eth.Signature), 126 | } 127 | 128 | require.NotNil(t, eth.Validate()) 129 | }) 130 | 131 | t.Run("signed by wallet, but put in eth.Signature", func(t *testing.T) { 132 | before_each(t) 133 | 134 | before_each(t) 135 | 136 | eth := generate() 137 | eth.Action = types.Actions.Delete 138 | eth.Signature, _ = mycrypto.SignPersonal([]byte(eth.GenerateSignPayload()), wallet_sk) 139 | eth.Extra = map[string]string{} 140 | 141 | require.NotNil(t, eth.Validate()) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /model/subkey.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/sha256" 6 | "encoding/json" 7 | "math/big" 8 | "time" 9 | 10 | "github.com/nextdotid/proof_server/types" 11 | "github.com/nextdotid/proof_server/util/crypto" 12 | "golang.org/x/xerrors" 13 | ) 14 | 15 | type Subkey struct { 16 | ID int64 `gorm:"primarykey"` 17 | CreatedAt time.Time `gorm:"not null"` 18 | Name string `gorm:"not null"` 19 | // Relying Party Identifier. 20 | RP_ID string `gorm:"rp_id"` 21 | 22 | Avatar string `gorm:"not null"` 23 | // Algorithm of this subkey 24 | Algorithm types.SubkeyAlgorithm `gorm:"algorithm"` 25 | // Public key of this subkey. 26 | PublicKey string `gorm:"public_key"` 27 | } 28 | 29 | type subkeySignPayload struct { 30 | Avatar string `json:"avatar"` 31 | Algorithm string `json:"algorithm"` 32 | PublicKey string `json:"public_key"` 33 | RP_ID string `json:"rp_id"` 34 | } 35 | 36 | func (Subkey) TableName() string { 37 | return "subkey" 38 | } 39 | 40 | // `self` doesn't needed to be stored in DB. 41 | func (self *Subkey) SignPayload() (payload string, err error) { 42 | if self.RP_ID == "" { 43 | return "", xerrors.Errorf("rp_id is empty") 44 | } 45 | avatarPK, err := crypto.StringToSecp256k1Pubkey(self.Avatar) 46 | if err != nil { 47 | return "", xerrors.Errorf("when parsing avatar public key: %w", err) 48 | } 49 | switch self.Algorithm { 50 | case types.SubkeyAlgorithms.Secp256R1: 51 | { 52 | _, err = crypto.StringToSecp256r1Pubkey(self.PublicKey) 53 | } 54 | case types.SubkeyAlgorithms.Secp256K1: 55 | { 56 | _, err = crypto.StringToSecp256k1Pubkey(self.PublicKey) 57 | } 58 | } 59 | if err != nil { 60 | return "", xerrors.Errorf("when parsing subkey public key: %w", err) 61 | } 62 | payloadStruct := subkeySignPayload{ 63 | Avatar: crypto.CompressedPubkeyHex(avatarPK), 64 | Algorithm: string(self.Algorithm), 65 | PublicKey: self.PublicKey, 66 | RP_ID: self.RP_ID, 67 | } 68 | 69 | payloadBytes, err := json.Marshal(payloadStruct) 70 | if err != nil { 71 | return "", xerrors.Errorf("when marshal JSON: %w", err) 72 | } 73 | return string(payloadBytes), nil 74 | } 75 | 76 | // ValidateSignature validates Avatar signature made to sign payload. 77 | func (self *Subkey) ValidateSignature(payload string, signature []byte) error { 78 | avatarPK, err := crypto.StringToSecp256k1Pubkey(self.Avatar) 79 | if err != nil { 80 | return xerrors.Errorf("when deserializing subkey: %w", err) 81 | } 82 | return crypto.ValidatePersonalSignature(payload, signature, avatarPK) 83 | } 84 | 85 | // For `Secp256K1` : signature should be made by `personal_sign()`. 86 | // For `Secp256R1` : signature should be made by ECDSA w/ SHA256 under P-256 curve 87 | func (self *Subkey) ValidateSubkeySignature(payload string, signature []byte) error { 88 | switch self.Algorithm { 89 | case types.SubkeyAlgorithms.Secp256K1: 90 | { 91 | subkeyPK, err := crypto.StringToSecp256k1Pubkey(self.PublicKey) 92 | if err != nil { 93 | return xerrors.Errorf("when deserializing subkey: %w", err) 94 | } 95 | return crypto.ValidatePersonalSignature(payload, signature, subkeyPK) 96 | } 97 | case types.SubkeyAlgorithms.Secp256R1: 98 | { 99 | pk, err := crypto.StringToSecp256r1Pubkey(self.PublicKey) 100 | if err != nil { 101 | return xerrors.Errorf("when deserializing subkey: %w", err) 102 | } 103 | hash := sha256.Sum256([]byte(payload)) 104 | r := new(big.Int).SetBytes(signature[:32]) 105 | s := new(big.Int).SetBytes(signature[32:]) 106 | if ecdsa.Verify(pk, hash[:], r, s) { 107 | return nil 108 | } else { 109 | return xerrors.New("signature validation failed") 110 | } 111 | } 112 | 113 | default: 114 | return xerrors.Errorf("algorithm not supported: %s", self.Algorithm) 115 | } 116 | } 117 | 118 | func (self *Subkey) Save(signature []byte) (id int64, err error) { 119 | signPayload, err := self.SignPayload() 120 | if err != nil { 121 | return 0, xerrors.Errorf("when generationg sign payload: %w", err) 122 | } 123 | if err := self.ValidateSignature(signPayload, signature); err != nil { 124 | return 0, xerrors.Errorf("when validating signature: %w", err) 125 | } 126 | 127 | self.CreatedAt = time.Now() 128 | tx := DB.Create(self) 129 | if tx.Error != nil { 130 | return 0, xerrors.Errorf("when saving record: %w", err) 131 | } 132 | 133 | return self.ID, nil 134 | } 135 | -------------------------------------------------------------------------------- /model/proof_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/nextdotid/proof_server/types" 8 | "github.com/nextdotid/proof_server/util" 9 | "github.com/nextdotid/proof_server/util/crypto" 10 | "github.com/nextdotid/proof_server/validator/twitter" 11 | "github.com/samber/lo" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func Test_Proof_Revalidate(t *testing.T) { 16 | t.Run("success", func(t *testing.T) { 17 | before_each(t) 18 | twitter.Init() 19 | 20 | pk, _ := crypto.StringToSecp256k1Pubkey("0x04666b700aeb6a6429f13cbb263e1bc566cd975a118b61bc796204109c1b351d19b7df23cc47f004e10fef41df82bad646b027578f8881f5f1d2f70c80dfcd8031") 21 | orig_created_at, _ := util.TimestampStringToTime("1647503071") 22 | pc := ProofChain{ 23 | Action: types.Actions.Create, 24 | Persona: MarshalAvatar(pk), 25 | Identity: "yeiwb", 26 | Location: "1504363098328924163", 27 | Platform: types.Platforms.Twitter, 28 | Signature: "D8i0UOXKrHJ23zCQe6USZDrw7fOjwm4R/eVX0AZXKgomynWWm+Px4Y7I1wtbsHwKj0t9psFqm87EnM93DXOmhwE=", 29 | Uuid: "c6fa1483-1bad-4f07-b661-678b191ab4b3", 30 | CreatedAt: orig_created_at, 31 | } 32 | tx := DB.Create(&pc) 33 | require.NoError(t, tx.Error) 34 | 35 | err := pc.Apply() 36 | require.Nil(t, err) 37 | 38 | proof := new(Proof) 39 | DB.Where("location = ?", pc.Location).Preload("ProofChain").Find(proof) 40 | require.NotEqual(t, proof.ID, 0) 41 | 42 | require.NoError(t, proof.Revalidate()) 43 | require.NotEmpty(t, proof.AltID, "should update AltID when revalidating") 44 | }) 45 | 46 | // t.Run("failure", func(t *testing.T) { 47 | // before_each(t) 48 | // twitter.Init() 49 | 50 | // pk, _ := crypto.StringToPubkey("0x028c3cda474361179d653c41a62f6bbb07265d535121e19aedf660da2924d0b1e3") 51 | // pc := ProofChain{ 52 | // Action: types.Actions.Create, 53 | // Persona: MarshalPersona(pk), 54 | // Identity: "yeiwb", 55 | // Location: "1469221200140574720", 56 | // Platform: types.Platforms.Twitter, 57 | // Signature: "gMUJ75eewkdaNrFp7bafzckv9+rlW7rVaxkB7/sYzYgFdFltYG+gn0lYzVNgrAdHWZPmu2giwJniGG7HG9iNigE=", 58 | // Uuid: uuid.New().String(), 59 | // } 60 | // tx := DB.Create(&pc) 61 | // require.Nil(t, tx.Error) 62 | 63 | // err := pc.Apply() 64 | // require.Nil(t, err) 65 | 66 | // proof := new(Proof) 67 | // DB.Where("location = ?", pc.Location).Preload("ProofChain").Find(proof) 68 | // require.NotEqual(t, proof.ID, 0) 69 | 70 | // require.Error(t, proof.Revalidate()) 71 | // }) 72 | } 73 | 74 | func Test_FindAllProofByPersona(t *testing.T) { 75 | t.Run("success", func(t *testing.T) { 76 | before_each(t) 77 | twitter.Init() 78 | 79 | pk, _ := crypto.StringToSecp256k1Pubkey("0x04666b700aeb6a6429f13cbb263e1bc566cd975a118b61bc796204109c1b351d19b7df23cc47f004e10fef41df82bad646b027578f8881f5f1d2f70c80dfcd8031") 80 | orig_created_at, _ := util.TimestampStringToTime("1647503071") 81 | pc := ProofChain{ 82 | Action: types.Actions.Create, 83 | Persona: MarshalAvatar(pk), 84 | Identity: "yeiwb", 85 | Location: "1504363098328924163", 86 | Platform: types.Platforms.Twitter, 87 | Signature: "D8i0UOXKrHJ23zCQe6USZDrw7fOjwm4R/eVX0AZXKgomynWWm+Px4Y7I1wtbsHwKj0t9psFqm87EnM93DXOmhwE=", 88 | Uuid: "c6fa1483-1bad-4f07-b661-678b191ab4b3", 89 | CreatedAt: orig_created_at, 90 | } 91 | tx := DB.Create(&pc) 92 | require.Nil(t, tx.Error) 93 | err := pc.Apply() 94 | require.Nil(t, err) 95 | 96 | pc2 := ProofChain{ 97 | Action: types.Actions.Create, 98 | Persona: MarshalAvatar(pk), 99 | Identity: "0x....", 100 | Location: "", 101 | Platform: types.Platforms.Ethereum, 102 | Signature: "D8i0UOXKrHJ23zCQe6USZDrw7fOjwm4R/eVX0AZXKgomynWWm+Px4Y7I1wtbsHwKj0t9psFqm87EnM93DXOmhwE=", 103 | Uuid: uuid.New().String(), 104 | CreatedAt: orig_created_at, 105 | } 106 | tx = DB.Create(&pc2) 107 | require.Nil(t, tx.Error) 108 | err = pc2.Apply() 109 | require.Nil(t, err) 110 | 111 | // 2 records should be created 112 | var count int64 113 | DB.Model(&Proof{}).Count(&count) 114 | require.Equal(t, int64(2), count) 115 | 116 | proofs, err := FindAllProofByPersona(pk, "id desc") 117 | require.Nil(t, err) 118 | require.Equal(t, 2, len(proofs)) 119 | result_types := lo.Map(proofs, func(proof Proof, _index int) types.Platform { 120 | return proof.Platform 121 | }) 122 | require.Contains(t, result_types, types.Platforms.Twitter) 123 | require.Contains(t, result_types, types.Platforms.Ethereum) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /validator/solana/solana.go: -------------------------------------------------------------------------------- 1 | package solana 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gagliardetto/solana-go" 7 | "github.com/mr-tron/base58" 8 | "github.com/nextdotid/proof_server/types" 9 | "github.com/nextdotid/proof_server/util" 10 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 11 | "github.com/nextdotid/proof_server/validator" 12 | "github.com/sirupsen/logrus" 13 | "golang.org/x/xerrors" 14 | ) 15 | 16 | type Solana struct { 17 | *validator.Base 18 | } 19 | 20 | var ( 21 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "solana"}) 22 | ) 23 | 24 | func Init() { 25 | if validator.PlatformFactories == nil { 26 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 27 | } 28 | validator.PlatformFactories[types.Platforms.Solana] = func(base *validator.Base) validator.IValidator { 29 | sol := Solana{base} 30 | return &sol 31 | } 32 | } 33 | 34 | func (*Solana) GeneratePostPayload() (post map[string]string) { 35 | return map[string]string{"default": ""} 36 | } 37 | 38 | func (sol *Solana) GenerateSignPayload() (payload string) { 39 | payloadStruct := validator.H{ 40 | "action": string(sol.Action), 41 | "identity": sol.Identity, 42 | "persona": "0x" + mycrypto.CompressedPubkeyHex(sol.Pubkey), 43 | "platform": "solana", 44 | "prev": nil, 45 | "created_at": util.TimeToTimestampString(sol.CreatedAt), 46 | "uuid": sol.Uuid.String(), 47 | } 48 | if sol.Previous != "" { 49 | payloadStruct["prev"] = sol.Previous 50 | } 51 | payloadBytes, err := json.Marshal(payloadStruct) 52 | if err != nil { 53 | l.Warnf("Error when marshalling struct: %s", err.Error()) 54 | return "" 55 | } 56 | return string(payloadBytes) 57 | } 58 | 59 | func (sol *Solana) Validate() (err error) { 60 | // Wallet Sig encoded by Base58 61 | // Persona Sig encoded by Base64 62 | sol.SignaturePayload = sol.GenerateSignPayload() 63 | sol.AltID = sol.Identity 64 | 65 | switch sol.Action { 66 | case types.Actions.Create: 67 | return sol.validateCreate() 68 | case types.Actions.Delete: 69 | return sol.validateDelete() 70 | default: 71 | return xerrors.Errorf("unknown action: %s", sol.Action) 72 | } 73 | } 74 | 75 | func (sol *Solana) validateCreate() (err error) { 76 | walletSig, ok := sol.Extra["wallet_signature"] 77 | if !ok { 78 | return xerrors.Errorf("wallet_signature not found") 79 | } 80 | 81 | if err := validateWalletSignature(sol.SignaturePayload, walletSig, sol.Identity); err != nil { 82 | return xerrors.Errorf("invalid wallet signature %w", err) 83 | } 84 | 85 | if err := mycrypto.ValidatePersonalSignature(sol.SignaturePayload, sol.Signature, sol.Pubkey); err != nil { 86 | return xerrors.Errorf("invalid persona signature %w", err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (solana *Solana) GetAltID() string { 93 | return solana.AltID 94 | } 95 | 96 | func (sol *Solana) validateDelete() (err error) { 97 | walletSig, ok := sol.Extra["wallet_signature"] 98 | 99 | // If wallet_signature exists, check it 100 | if ok && walletSig != "" { 101 | err := validateWalletSignature(sol.SignaturePayload, walletSig, sol.Identity) 102 | if err != nil { 103 | return xerrors.Errorf("invalid wallet signature %w", err) 104 | } 105 | 106 | sigBytes, err := base58.Decode(walletSig) 107 | if err != nil { 108 | return xerrors.Errorf("invalid wallet signature format %w", err) 109 | } 110 | 111 | sol.Signature = sigBytes 112 | return nil 113 | } 114 | 115 | if err := mycrypto.ValidatePersonalSignature(sol.SignaturePayload, sol.Signature, sol.Pubkey); err != nil { 116 | return xerrors.Errorf("invalid persona signature %w", err) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func validateWalletSignature(payload, sig, address string) error { 123 | pubkey, err := solana.PublicKeyFromBase58(address) 124 | if err != nil { 125 | return xerrors.Errorf("error when decoding pubkey: %w", err) 126 | } 127 | 128 | signature, err := solana.SignatureFromBase58(sig) 129 | if err != nil { 130 | return xerrors.Errorf("error when decoding signature: %w", err) 131 | } 132 | 133 | // It is simply an UTF8 byte array representing the payload text 134 | // Furthermore, the byte array will be hashed with SHA512 according to tweetnacl docs 135 | // See https://github.com/solana-labs/wallet-adapter/blob/master/FAQ.md#how-can-i-sign-and-verify-messages 136 | if !signature.Verify(pubkey, []byte(payload)) { 137 | return xerrors.Errorf("solana wallet signature validation failed") 138 | } 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /validator/github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/nextdotid/proof_server/types" 11 | "github.com/nextdotid/proof_server/util" 12 | "github.com/nextdotid/proof_server/util/crypto" 13 | "github.com/nextdotid/proof_server/validator" 14 | "github.com/sirupsen/logrus" 15 | "golang.org/x/xerrors" 16 | 17 | ghub "github.com/google/go-github/v41/github" 18 | ) 19 | 20 | type Github struct { 21 | *validator.Base 22 | } 23 | 24 | type gistPayload struct { 25 | Version string `json:"version"` 26 | Comment string `json:"comment"` 27 | Comment2 string `json:"comment2"` 28 | Persona string `json:"persona"` 29 | GithubUsername string `json:"github_username"` 30 | SignPayload string `json:"sign_payload"` 31 | Signature string `json:"signature"` 32 | CreatedAt string `json:"created_at"` 33 | Uuid string `json:"uuid"` 34 | } 35 | 36 | var ( 37 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "github"}) 38 | ) 39 | 40 | func Init() { 41 | if validator.PlatformFactories == nil { 42 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 43 | } 44 | validator.PlatformFactories[types.Platforms.Github] = func(base *validator.Base) validator.IValidator { 45 | gh := Github{base} 46 | return &gh 47 | } 48 | } 49 | 50 | func (gh *Github) GeneratePostPayload() (post map[string]string) { 51 | gh.Identity = strings.ToLower(gh.Identity) 52 | payload := gistPayload{ 53 | Version: "1", 54 | Comment: "Here's an NextID proof of this Github account.", 55 | Comment2: "To validate, base64.decode the signature, and recover pubkey from it using sign_payload with ethereum personal_sign algo.", 56 | Persona: "0x" + crypto.CompressedPubkeyHex(gh.Pubkey), 57 | GithubUsername: gh.Identity, 58 | SignPayload: gh.GenerateSignPayload(), 59 | Signature: "%SIG_BASE64%", 60 | CreatedAt: util.TimeToTimestampString(gh.CreatedAt), 61 | Uuid: gh.Uuid.String(), 62 | } 63 | 64 | payload_json, _ := json.MarshalIndent(payload, "", "\t") 65 | return map[string]string{"default": string(payload_json)} 66 | } 67 | 68 | func (gh *Github) GenerateSignPayload() (payload string) { 69 | gh.Identity = strings.ToLower(gh.Identity) 70 | payloadStruct := validator.H{ 71 | "action": string(gh.Action), 72 | "identity": gh.Identity, 73 | "platform": string(types.Platforms.Github), 74 | "prev": nil, 75 | "created_at": util.TimeToTimestampString(gh.CreatedAt), 76 | "uuid": gh.Uuid.String(), 77 | } 78 | if gh.Previous != "" { 79 | payloadStruct["prev"] = gh.Previous 80 | } 81 | 82 | payload_bytes, _ := json.Marshal(payloadStruct) 83 | return string(payload_bytes) 84 | } 85 | 86 | func (gh *Github) Validate() (err error) { 87 | gh.Identity = strings.ToLower(gh.Identity) 88 | gh.SignaturePayload = gh.GenerateSignPayload() 89 | 90 | client := ghub.NewClient(nil) 91 | gist, response, err := client.Gists.Get(context.TODO(), gh.ProofLocation) 92 | if err != nil { 93 | return xerrors.Errorf("error when fetching gist: %w", err) 94 | } 95 | 96 | if response.StatusCode != 200 { 97 | return xerrors.Errorf("error when fetching gist") 98 | } 99 | 100 | if gh.Identity != gist.Owner.GetLogin() { 101 | return xerrors.Errorf("gist owner mismatch: should be %s, but got %s", gh.Identity, gist.Owner.GetLogin()) 102 | } 103 | gh.AltID = strconv.FormatInt(gist.Owner.GetID(), 10) 104 | 105 | gist_filename := fmt.Sprintf("0x%s.json", crypto.CompressedPubkeyHex(gh.Pubkey)) 106 | files := gist.GetFiles() 107 | content := "" 108 | for filename, file := range files { 109 | if filename != ghub.GistFilename(gist_filename) { 110 | continue 111 | } 112 | 113 | content = *file.Content 114 | } 115 | if content == "" { 116 | return xerrors.Errorf("%s not found or empty", gist_filename) 117 | } 118 | payload := gistPayload{} 119 | err = json.Unmarshal([]byte(content), &payload) 120 | if err != nil { 121 | return xerrors.Errorf("error when parsing JSON: %w", err) 122 | } 123 | 124 | pubkey_recovered, err := crypto.StringToSecp256k1Pubkey(payload.Persona) 125 | if err != nil { 126 | return xerrors.Errorf("error when recovering pubkey: %w", err) 127 | } 128 | signature, err := util.DecodeString(payload.Signature) 129 | if err != nil { 130 | return xerrors.Errorf("error when decoding signature: %w", err) 131 | } 132 | return crypto.ValidatePersonalSignature(payload.SignPayload, signature, pubkey_recovered) 133 | } 134 | 135 | func (gh *Github) GetAltID() string { 136 | return gh.AltID 137 | } 138 | -------------------------------------------------------------------------------- /validator/tiktok/fetcher.go: -------------------------------------------------------------------------------- 1 | package tiktok 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "regexp" 9 | 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | const ( 14 | FINAL_URL_TEMPLATE = "^https://www\\.tiktok\\.com/@(.+?)/video/(\\d+)" 15 | OEMBED_URL_BASE = "https://www.tiktok.com/oembed?url=%s" 16 | ) 17 | 18 | var ( 19 | finalUrlRegexp = regexp.MustCompile(FINAL_URL_TEMPLATE) 20 | ) 21 | 22 | type OEmbedInfo struct { 23 | // "version": "1.0", 24 | Version string `json:"version"` 25 | // "type": "video", 26 | Type string `json:"type"` 27 | // "title": "Scramble up ur name & I’ll try to guess it😍❤️ #foryoupage #petsoftiktok #aesthetic", 28 | Title string `json:"title"` 29 | // "author_url": "https://www.tiktok.com/@menyki", 30 | AuthorURL string `json:"author_url"` 31 | // "author_name": "Scout & Suki", 32 | AuthorName string `json:"author_name"` 33 | // author_unique_id : "menyki" 34 | AuthorUniqueID string `json:"author_unique_id"` 35 | // "width": "100%", 36 | Width string `json:"width"` 37 | // "height": "100%", 38 | Height string `json:"height"` 39 | // "html": "
MAX_REDIRECT { 98 | return "", "", xerrors.Errorf("tiktok: too much redirect") 99 | } 100 | if username, videoID = parseFinalURL(url); username != "" { 101 | return username, videoID, nil 102 | } 103 | 104 | req, err := http.NewRequest("GET", url, nil) 105 | if err != nil { 106 | return "", "", xerrors.Errorf("tiktok: HTTP error: %w", err) 107 | } 108 | req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") 109 | resp, err := (&http.Client{ 110 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 111 | return http.ErrUseLastResponse 112 | }, 113 | }).Do(req) 114 | if err != nil { 115 | return "", "", xerrors.Errorf("tiktok: HTTP error: %w", err) 116 | } 117 | 118 | redirectLocation, err := resp.Location() 119 | if redirectLocation != nil { 120 | return redirectToFinalURL(redirectLocation.String(), redirectCount+1) 121 | } 122 | return "", "", xerrors.Errorf("tiktok: not a valid URL") 123 | } 124 | 125 | func parseFinalURL(url string) (username, videoID string) { 126 | result := finalUrlRegexp.FindStringSubmatch(url) 127 | if len(result) != 3 { 128 | return "", "" 129 | } 130 | return result[1], result[2] 131 | } 132 | -------------------------------------------------------------------------------- /validator/keybase/keybase.go: -------------------------------------------------------------------------------- 1 | package keybase 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/nextdotid/proof_server/types" 11 | "github.com/nextdotid/proof_server/util" 12 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 13 | "github.com/nextdotid/proof_server/validator" 14 | "github.com/sirupsen/logrus" 15 | "golang.org/x/xerrors" 16 | ) 17 | 18 | type Keybase struct { 19 | *validator.Base 20 | } 21 | 22 | type KeybasePayload struct { 23 | Version string `json:"version"` 24 | Comment string `json:"comment"` 25 | Comment2 string `json:"comment2"` 26 | Persona string `json:"persona"` 27 | KeybaseUsername string `json:"keybase_username"` 28 | SignPayload string `json:"sign_payload"` 29 | Signature string `json:"signature"` 30 | CreatedAt string `json:"created_at"` 31 | Uuid string `json:"uuid"` 32 | } 33 | 34 | const ( 35 | URL = "https://%s.keybase.pub/NextID/0x%s.json" 36 | ) 37 | 38 | var ( 39 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "keybase"}) 40 | ) 41 | 42 | func Init() { 43 | if validator.PlatformFactories == nil { 44 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 45 | } 46 | validator.PlatformFactories[types.Platforms.Keybase] = func(base *validator.Base) validator.IValidator { 47 | kb := Keybase{base} 48 | return &kb 49 | } 50 | } 51 | 52 | func (kb *Keybase) GeneratePostPayload() (post map[string]string) { 53 | kb.Identity = strings.ToLower(kb.Identity) 54 | payload := KeybasePayload{ 55 | Version: "1", 56 | Comment: "Here's an NextID proof of this Keybase account.", 57 | Comment2: "To validate, base64.decode the signature, and recover pubkey from it using sign_payload with ethereum personal_sign algo.", 58 | Persona: "0x" + mycrypto.CompressedPubkeyHex(kb.Pubkey), 59 | KeybaseUsername: kb.Identity, 60 | SignPayload: kb.GenerateSignPayload(), 61 | Signature: "%SIG_BASE64%", 62 | CreatedAt: util.TimeToTimestampString(kb.CreatedAt), 63 | Uuid: kb.Uuid.String(), 64 | } 65 | payload_json, _ := json.MarshalIndent(payload, "", "\t") 66 | return map[string]string{"default": string(payload_json)} 67 | } 68 | 69 | func (kb *Keybase) GenerateSignPayload() (payload string) { 70 | kb.Identity = strings.ToLower(kb.Identity) 71 | payloadStruct := validator.H{ 72 | "action": string(kb.Action), 73 | "identity": kb.Identity, 74 | "platform": string(types.Platforms.Keybase), 75 | "prev": nil, 76 | "created_at": util.TimeToTimestampString(kb.CreatedAt), 77 | "uuid": kb.Uuid.String(), 78 | } 79 | if kb.Previous != "" { 80 | payloadStruct["prev"] = kb.Previous 81 | } 82 | 83 | payloadBytes, err := json.Marshal(payloadStruct) 84 | if err != nil { 85 | l.Warnf("Error when marshaling struct: %s", err.Error()) 86 | return "" 87 | } 88 | 89 | return string(payloadBytes) 90 | } 91 | 92 | func (kb *Keybase) Validate() (err error) { 93 | kb.Identity = strings.ToLower(kb.Identity) 94 | kb.SignaturePayload = kb.GenerateSignPayload() 95 | kb.AltID = kb.Identity // TODO: maybe get Keybase UserID in another API call? 96 | 97 | url := fmt.Sprintf(URL, kb.Identity, mycrypto.CompressedPubkeyHex(kb.Pubkey)) 98 | kb.ProofLocation = url 99 | resp, err := http.Get(url) 100 | if err != nil { 101 | return xerrors.Errorf("Error when requesting proof: %s", err.Error()) 102 | } 103 | if resp.StatusCode != 200 { 104 | return xerrors.Errorf("Error when requesting proof: Status code %d", resp.StatusCode) 105 | } 106 | body, err := io.ReadAll(resp.Body) 107 | if err != nil { 108 | return xerrors.Errorf("Error when getting resp body") 109 | } 110 | 111 | payload := new(KeybasePayload) 112 | err = json.Unmarshal(body, payload) 113 | if err != nil { 114 | return xerrors.Errorf("error when decoding JSON: %w", err) 115 | } 116 | return kb.validateBody(payload) 117 | } 118 | 119 | func (kb *Keybase) GetAltID() string { 120 | // TODO: implement AltID for Keybase: 121 | // https://keybase.io/_/api/1.0/user/lookup.json?usernames=chris 122 | // jq .them[0].id 123 | return kb.AltID 124 | } 125 | 126 | func (kb *Keybase) validateBody(payload *KeybasePayload) error { 127 | if payload.Persona != ("0x" + mycrypto.CompressedPubkeyHex(kb.Pubkey)) { 128 | return xerrors.Errorf("Persona mismatch") 129 | } 130 | 131 | sig_bytes, err := util.DecodeString(payload.Signature) 132 | if err != nil { 133 | return xerrors.Errorf("error when decoding sig: %w", err) 134 | } 135 | 136 | kb.Signature = sig_bytes 137 | return mycrypto.ValidatePersonalSignature(kb.SignaturePayload, sig_bytes, kb.Pubkey) 138 | } 139 | -------------------------------------------------------------------------------- /controller/proof_upload.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "encoding/base64" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/google/uuid" 10 | "github.com/nextdotid/proof_server/model" 11 | "github.com/nextdotid/proof_server/types" 12 | "github.com/nextdotid/proof_server/util" 13 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 14 | "github.com/nextdotid/proof_server/util/sqs" 15 | "github.com/nextdotid/proof_server/validator" 16 | "golang.org/x/xerrors" 17 | ) 18 | 19 | type ProofUploadRequest struct { 20 | Action types.Action `json:"action"` 21 | Platform types.Platform `json:"platform"` 22 | Identity string `json:"identity"` 23 | ProofLocation string `json:"proof_location"` 24 | PublicKey string `json:"public_key"` 25 | Uuid string `json:"uuid"` 26 | CreatedAt string `json:"created_at"` 27 | Extra ProofUploadRequestExtra `json:"extra"` 28 | } 29 | 30 | type ProofUploadRequestExtra struct { 31 | Signature string `json:"signature"` 32 | EthereumWalletSignature string `json:"wallet_signature"` 33 | } 34 | 35 | func proofUpload(c *gin.Context) { 36 | req := ProofUploadRequest{} 37 | err := c.BindJSON(&req) 38 | if err != nil { 39 | errorResp(c, 400, xerrors.Errorf("parse request failed: %w", err)) 40 | return 41 | } 42 | 43 | l.Infof("UPLOADING PROOF: %+v", req) 44 | pubkey, err := mycrypto.StringToSecp256k1Pubkey(req.PublicKey) 45 | if err != nil { 46 | errorResp(c, 400, xerrors.Errorf("%w", err)) 47 | return 48 | } 49 | 50 | previous_pc, err := model.ProofChainFindLatest(mycrypto.CompressedPubkeyHex(pubkey)) 51 | if err != nil { 52 | errorResp(c, 500, xerrors.Errorf("internal database error")) 53 | return 54 | } 55 | 56 | validator, err := validateProof(req, previous_pc, pubkey) 57 | if err != nil { 58 | errorResp(c, 400, xerrors.Errorf("%w", err)) 59 | return 60 | } 61 | 62 | if err = applyUpload(&validator); err != nil { 63 | errorResp(c, 400, xerrors.Errorf("%w", err)) 64 | return 65 | } 66 | 67 | if err = triggerArweave(model.MarshalAvatar(pubkey)); err != nil { 68 | // Do not errorResp here, since it is a tolerable error. 69 | l.Warnf("error sending arweave upload message: %v", err) 70 | } 71 | 72 | c.JSON(http.StatusCreated, gin.H{}) 73 | } 74 | 75 | func validateProof(req ProofUploadRequest, prev *model.ProofChain, pubkey *ecdsa.PublicKey) (validator.Base, error) { 76 | prev_signature := "" 77 | if prev != nil { 78 | prev_signature = prev.Signature 79 | } 80 | 81 | performer_factory, ok := validator.PlatformFactories[req.Platform] 82 | if !ok { 83 | return validator.Base{}, xerrors.Errorf("platform not supported: %s", string(req.Platform)) 84 | } 85 | created_at, err := util.TimestampStringToTime(req.CreatedAt) 86 | if err != nil { 87 | return validator.Base{}, xerrors.Errorf("error when parsing created_at: %s not recognized", req.CreatedAt) 88 | } 89 | parsed_uuid, err := uuid.Parse(req.Uuid) 90 | if err != nil { 91 | return validator.Base{}, xerrors.Errorf("error when parsing uuid: %s not recognized", req.Uuid) 92 | } 93 | base := validator.Base{ 94 | Platform: req.Platform, 95 | Previous: prev_signature, 96 | Action: req.Action, 97 | Pubkey: pubkey, 98 | Identity: req.Identity, 99 | ProofLocation: req.ProofLocation, 100 | CreatedAt: created_at, 101 | Uuid: parsed_uuid, 102 | } 103 | 104 | if req.Extra.Signature != "" || req.Platform == types.Platforms.Ethereum { 105 | extra := map[string]string{} 106 | extra["wallet_signature"] = req.Extra.EthereumWalletSignature 107 | base.Extra = extra 108 | 109 | persona_sig, err := base64.StdEncoding.DecodeString(req.Extra.Signature) 110 | if err != nil { 111 | return validator.Base{}, xerrors.Errorf("error when decoding persona signature: %w", err) 112 | } 113 | base.Signature = persona_sig 114 | } 115 | 116 | performer := performer_factory(&base) 117 | return base, performer.Validate() 118 | } 119 | 120 | func applyUpload(validator *validator.Base) error { 121 | pc, err := model.ProofChainCreateFromValidator(validator) 122 | if err != nil { 123 | return xerrors.Errorf("%w", err) 124 | } 125 | 126 | err = pc.Apply() 127 | if err != nil { 128 | return xerrors.Errorf("%w", err) 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func triggerArweave(persona string) error { 135 | msg := types.QueueMessage{ 136 | Action: types.QueueActions.ArweaveUpload, 137 | Persona: persona, 138 | } 139 | 140 | if err := sqs.Send(msg); err != nil { 141 | return xerrors.Errorf("error sending message to queue: %w", err) 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /validator/twitter/twitter.go: -------------------------------------------------------------------------------- 1 | package twitter 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/nextdotid/proof_server/types" 12 | "github.com/nextdotid/proof_server/util" 13 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 14 | "github.com/sirupsen/logrus" 15 | "golang.org/x/xerrors" 16 | 17 | "github.com/nextdotid/proof_server/validator" 18 | ) 19 | 20 | type Twitter struct { 21 | *validator.Base 22 | } 23 | 24 | const ( 25 | HEADLESS_MATCH_TEMPLATE = "\\bSig: (.+?)[\\s\\n]" 26 | MATCH_TEMPLATE = "^Sig: (.+?)$" 27 | MATCH_POST_CONTENT = `^Misc: ([^|]+)\|(\d+)\|(.+)?$` 28 | ) 29 | 30 | var ( 31 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "twitter"}) 32 | re = regexp.MustCompile(MATCH_TEMPLATE) 33 | POST_STRUCT = map[string]string{ 34 | // Misc info: UUID|CreatedAt|Previous 35 | "default": "🎭 Verify @%s with @NextDotID.\nSig: %%SIG_BASE64%%\nMisc: %s|%s|%s", 36 | "en_US": "🎭 Verify @%s with @NextDotID.\nSig: %%SIG_BASE64%%\nMisc: %s|%s|%s", 37 | "zh_CN": "🎭 由 @NextDotID 验证 @%s 。\nSig: %%SIG_BASE64%%\n其它信息: %s|%s|%s", 38 | } 39 | ) 40 | 41 | func Init() { 42 | if validator.PlatformFactories == nil { 43 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 44 | } 45 | 46 | validator.PlatformFactories[types.Platforms.Twitter] = func(base *validator.Base) validator.IValidator { 47 | twi := Twitter{base} 48 | return &twi 49 | } 50 | } 51 | 52 | func (twitter *Twitter) GeneratePostPayload() (post map[string]string) { 53 | post = make(map[string]string, 0) 54 | for lang_code, template := range POST_STRUCT { 55 | post[lang_code] = fmt.Sprintf(template, twitter.Identity, twitter.Uuid.String(), util.TimeToTimestampString(twitter.CreatedAt), twitter.Previous) 56 | } 57 | 58 | return post 59 | } 60 | 61 | func (twitter *Twitter) GenerateSignPayload() (payload string) { 62 | twitter.Identity = strings.ToLower(twitter.Identity) 63 | payloadStruct := validator.H{ 64 | "action": string(twitter.Action), 65 | "identity": twitter.Identity, 66 | "platform": "twitter", 67 | "prev": nil, 68 | "created_at": util.TimeToTimestampString(twitter.CreatedAt), 69 | "uuid": twitter.Uuid.String(), 70 | } 71 | if twitter.Previous != "" { 72 | payloadStruct["prev"] = twitter.Previous 73 | } 74 | 75 | payloadBytes, err := json.Marshal(payloadStruct) 76 | if err != nil { 77 | l.Warnf("Error when marshaling struct: %s", err.Error()) 78 | return "" 79 | } 80 | 81 | return string(payloadBytes) 82 | } 83 | 84 | func (twitter *Twitter) Validate() (err error) { 85 | twitter.Identity = strings.ToLower(twitter.Identity) 86 | if twitter.SignaturePayload == "" { 87 | twitter.SignaturePayload = twitter.GenerateSignPayload() 88 | } 89 | 90 | // Deletion. No need to fetch tweet. 91 | if twitter.Action == types.Actions.Delete { 92 | return mycrypto.ValidatePersonalSignature(twitter.SignaturePayload, twitter.Signature, twitter.Pubkey) 93 | } 94 | 95 | tweetID, err := strconv.ParseInt(twitter.ProofLocation, 10, 64) 96 | if err != nil { 97 | return xerrors.Errorf("parsing tweet ID %s: %s", twitter.ProofLocation, err.Error()) 98 | } 99 | 100 | // post, err := validator.GetPostWithHeadlessBrowser( 101 | // fmt.Sprintf("https://twitter.com/%s/status/%d", twitter.Identity, tweetID), 102 | // "Sig:", 103 | // ) 104 | // if err != nil { 105 | // return xerrors.Errorf("fetching tweet with headless browser: %w", err) 106 | // } 107 | 108 | tweet, err := fetchPostWithAPI(fmt.Sprint(tweetID), 3) 109 | if err != nil { 110 | return xerrors.Errorf("fetching tweet with syndication API: %w", err) 111 | } 112 | if twitter.Identity != tweet.User.ScreenName { 113 | return xerrors.Errorf("tweet is not sent by this account.") 114 | } 115 | twitter.Text = tweet.Text 116 | twitter.AltID = tweet.User.ID 117 | return twitter.validateText() 118 | } 119 | 120 | func (twitter *Twitter) GetAltID() string { 121 | return twitter.AltID 122 | } 123 | 124 | func (twitter *Twitter) validateText() (err error) { 125 | scanner := bufio.NewScanner(strings.NewReader(twitter.Text)) 126 | for scanner.Scan() { 127 | matched := re.FindStringSubmatch(scanner.Text()) 128 | if len(matched) < 2 { 129 | continue // Search for next line 130 | } 131 | 132 | sigBase64 := matched[1] 133 | sigBytes, err := util.DecodeString(sigBase64) 134 | if err != nil { 135 | return xerrors.Errorf("decoding signature %s: %s", sigBase64, err.Error()) 136 | } 137 | twitter.Signature = sigBytes 138 | return mycrypto.ValidatePersonalSignature(twitter.SignaturePayload, sigBytes, twitter.Pubkey) 139 | } 140 | return xerrors.Errorf("Signature not found in tweet text.") 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build binary and deploy to official server 2 | on: 3 | push: 4 | branches: [master, develop] 5 | 6 | jobs: 7 | build: 8 | concurrency: ${{ matrix.appcmd }} 9 | strategy: 10 | matrix: 11 | go: ['1.21'] 12 | appcmd: ['server', 'lambda', 'lambda_worker'] 13 | appenv: ['staging', 'production'] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v3 18 | - name: Setup Go v1.x 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: ${{ matrix.go }} 22 | cache: true 23 | - name: Build 24 | env: 25 | CGO_ENABLED: '0' 26 | GOARCH: amd64 27 | GOOS: linux 28 | run: go build -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=${{ matrix.appenv }}' -X 'github.com/nextdotid/proof_server/common.Revision=${{ github.sha }}' -X 'github.com/nextdotid/proof_server/common.BuildTime=$(date +%s)'" -o ./build/${{ matrix.appcmd }}_${{ matrix.appenv }} ./cmd/${{ matrix.appcmd }} 29 | 30 | - name: Upload artifact 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: ${{ matrix.appcmd }}_${{ matrix.appenv }} 34 | path: | 35 | build/${{ matrix.appcmd }}_${{ matrix.appenv }} 36 | retention-days: 3 37 | 38 | deploy-staging: 39 | needs: build 40 | name: Deploy to AWS Lambda (staging) 41 | if: github.ref_name == 'develop' && github.repository == 'nextdotid/proof_server' 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Download lambda binary 45 | uses: actions/download-artifact@v3 46 | with: 47 | name: lambda_staging 48 | - name: Download lambda worker binary 49 | uses: actions/download-artifact@v3 50 | with: 51 | name: lambda_worker_staging 52 | 53 | - name: Package lambda binary into zip 54 | run: | 55 | mv lambda_staging lambda 56 | mv lambda_worker_staging worker 57 | chmod a+x lambda 58 | zip lambda.zip lambda 59 | chmod a+x worker 60 | zip worker.zip worker 61 | - uses: actions/setup-python@v3 62 | with: 63 | python-version: '3.x' 64 | - name: Deploy to AWS Lambda (main) 65 | run: | 66 | pip3 install awscli 67 | aws lambda update-function-code --function-name ${{ secrets.AWS_LAMBDA_NAME_STAGING }} --zip-file 'fileb://./lambda.zip' > /dev/null 68 | env: 69 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 70 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 71 | AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} 72 | - name: Deploy to AWS Lambda (worker) 73 | run: | 74 | pip3 install awscli 75 | aws lambda update-function-code --function-name ${{ secrets.AWS_LAMBDA_WORKER_NAME_STAGING }} --zip-file 'fileb://./worker.zip' > /dev/null 76 | env: 77 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 78 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 79 | AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} 80 | 81 | deploy-production: 82 | needs: build 83 | name: Deploy to AWS Lambda (production) 84 | if: github.ref_name == 'master' && github.repository == 'nextdotid/proof_server' 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Download lambda binary 88 | uses: actions/download-artifact@v3 89 | with: 90 | name: lambda_production 91 | - name: Download lambda worker binary 92 | uses: actions/download-artifact@v3 93 | with: 94 | name: lambda_worker_production 95 | 96 | - name: Package lambda binary into zip 97 | run: | 98 | mv lambda_production lambda 99 | mv lambda_worker_production worker 100 | chmod a+x lambda worker 101 | zip lambda.zip lambda 102 | zip worker.zip worker 103 | - uses: actions/setup-python@v3 104 | with: 105 | python-version: '3.x' 106 | - name: Deploy to AWS Lambda 107 | run: | 108 | pip3 install awscli 109 | aws lambda update-function-code --function-name ${{ secrets.AWS_LAMBDA_NAME_PRODUCTION }} --zip-file 'fileb://./lambda.zip' > /dev/null 110 | env: 111 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 112 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 113 | AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} 114 | - name: Deploy to AWS Lambda (worker) 115 | run: | 116 | pip3 install awscli 117 | aws lambda update-function-code --function-name ${{ secrets.AWS_LAMBDA_WORKER_NAME_PRODUCTION }} --zip-file 'fileb://./worker.zip' > /dev/null 118 | env: 119 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 120 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 121 | AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} 122 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test 2 | 3 | bin_dir=build/ 4 | commit=$$(git rev-parse HEAD) 5 | time=$$(date +%s) 6 | aws_registry_uri=${aws-account-id}.dkr.ecr.${aws-lambda-region}.amazonaws.com 7 | aws_docker_image=${aws_registry_uri}/${docker-image-name}:${commit} 8 | 9 | # Things in my.mk: 10 | # aws-lambda-function-staging=my-lambda-function-staging 11 | # aws-lambda-function-production=my-lambda-function-production 12 | # aws-lambda-headless-function-staging=my-lambda-headless-function-staging 13 | # aws-lambda-region=ap-east-1 14 | # aws-lambda-role=arn:aws:iam::xxxxx:.... 15 | # aws-account-id=xxxxxxxxxx 16 | # docker-image-name=lambda_headless 17 | 18 | -include ./my.mk 19 | 20 | build: 21 | @go build \ 22 | -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=develop' -X 'github.com/nextdotid/proof_server/common.Revision=${commit}' -X 'github.com/nextdotid/proof_server/common.BuildTime=${time}'" \ 23 | -o ${bin_dir} ./cmd/... 24 | 25 | test: 26 | @go test -v ./... 27 | 28 | lambda-build-production: 29 | @go clean 30 | @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \ 31 | -v -x \ 32 | -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=production' -X 'github.com/nextdotid/proof_server/common.Revision=${commit}' -X 'github.com/nextdotid/proof_server/common.BuildTime=${time}'" \ 33 | -o ./build/lambda \ 34 | ./cmd/lambda 35 | 36 | lambda-build-staging: 37 | @go clean 38 | @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \ 39 | -v -x \ 40 | -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=staging' -X 'github.com/nextdotid/proof_server/common.Revision=${commit}' -X 'github.com/nextdotid/proof_server/common.BuildTime=${time}'" \ 41 | -o ./build/lambda \ 42 | ./cmd/lambda 43 | 44 | lambda-pack-staging: lambda-build-staging 45 | @cd ./build && zip lambda.zip lambda 46 | 47 | lambda-pack-production: lambda-build-production 48 | @cd ./build && zip lambda.zip lambda 49 | 50 | lambda-update-staging: lambda-pack-staging 51 | @aws lambda update-function-code --function-name ${aws-lambda-function-staging} --zip-file 'fileb://./build/lambda.zip' 52 | 53 | lambda-update-production: lambda-pack-production 54 | @aws lambda update-function-code --function-name ${aws-lambda-function-production} --zip-file 'fileb://./build/lambda.zip' 55 | 56 | lambda-create-staging: lambda-pack-staging 57 | aws lambda create-function \ 58 | --region ${aws-lambda-region} \ 59 | --function-name ${aws-lambda-function-staging} \ 60 | --handler lambda \ 61 | --role ${aws-lambda-role} \ 62 | --runtime go1.x \ 63 | --zip-file "fileb://./build/lambda.zip" 64 | 65 | lambda-create-production: lambda-pack-production 66 | aws lambda create-function \ 67 | --region ${aws-lambda-region} \ 68 | --function-name ${aws-lambda-function-production} \ 69 | --handler lambda \ 70 | --role ${aws-lambda-role} \ 71 | --runtime go1.x \ 72 | --zip-file "fileb://./build/lambda.zip" 73 | 74 | lambda-build-worker-staging: 75 | @go clean 76 | @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \ 77 | -v -x \ 78 | -ldflags "-X 'github.com/nextdotid/proof_server/common.Environment=staging' -X 'github.com/nextdotid/proof_server/common.Revision=${commit}' -X 'github.com/nextdotid/proof_server/common.BuildTime=${time}'" \ 79 | -o ./build/lambda \ 80 | ./cmd/lambda_worker 81 | 82 | lambda-pack-worker-staging: lambda-build-worker-staging 83 | @cd ./build && zip lambda.zip lambda 84 | 85 | lambda-update-worker-staging: lambda-pack-worker-staging 86 | @aws lambda update-function-code --function-name ${aws-lambda-function-worker-staging} --zip-file 'fileb://./build/lambda.zip' 87 | 88 | lamda-create-registry-headless: 89 | @aws ecr get-login-password \ 90 | --region ${aws-lambda-region} | docker login \ 91 | --username AWS \ 92 | --password-stdin ${aws_registry_uri} 93 | @aws ecr describe-repositories \ 94 | --repository-names ${docker-image-name} || \ 95 | aws ecr create-repository \ 96 | --repository-name ${docker-image-name} \ 97 | --region ${aws-lambda-region} \ 98 | --image-scanning-configuration scanOnPush=true \ 99 | --image-tag-mutability MUTABLE 100 | 101 | lambda-build-headless-staging: 102 | @docker build -f ./cmd/lambda_headless/Dockerfile -t ${docker-image-name}:${commit} . 103 | @docker tag ${docker-image-name}:${commit} ${aws_docker_image} 104 | 105 | lambda-pack-headless-staging: lamda-create-registry-headless lambda-build-headless-staging 106 | @docker push ${aws_docker_image} 107 | 108 | lambda-create-headless-staging: lambda-pack-headless-staging 109 | aws lambda create-function \ 110 | --package-type Image \ 111 | --region ${aws-lambda-region} \ 112 | --function-name ${aws-lambda-headless-function-staging} \ 113 | --code ImageUri=${aws_docker_image} \ 114 | --memory-size 1200 \ 115 | --timeout 30 \ 116 | --architectures x86_64 \ 117 | --role ${aws-lambda-role} 118 | 119 | lambda-update-headless-staging: lambda-pack-headless-staging 120 | @aws lambda update-function-code --function-name ${aws-lambda-headless-function-staging} --image-uri ${aws_docker_image} 121 | -------------------------------------------------------------------------------- /validator/das/das.go: -------------------------------------------------------------------------------- 1 | package das 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/nextdotid/proof_server/types" 11 | "github.com/nextdotid/proof_server/util" 12 | mycrypto "github.com/nextdotid/proof_server/util/crypto" 13 | "github.com/nextdotid/proof_server/validator" 14 | "github.com/samber/lo" 15 | "github.com/sirupsen/logrus" 16 | "golang.org/x/xerrors" 17 | ) 18 | 19 | type Das struct { 20 | *validator.Base 21 | } 22 | 23 | type DasSignPayload struct { 24 | Version string `json:"version"` 25 | Comment string `json:"comment"` 26 | Comment2 string `json:"comment2"` 27 | Persona string `json:"persona"` 28 | BitAddress string `json:"bit_address"` 29 | SignPayload string `json:"sign_payload"` 30 | Signature string `json:"signature"` 31 | CreatedAt string `json:"created_at"` 32 | Uuid string `json:"uuid"` 33 | } 34 | 35 | type DasRequest struct { 36 | Account string `json:"account"` 37 | } 38 | 39 | type DasResponse struct { 40 | ErrorNumber int `json:"err_no"` 41 | ErrorMessage string `json:"err_msg"` 42 | Data struct { 43 | Records []DasRecord `json:"records"` 44 | } 45 | } 46 | 47 | type DasRecord struct { 48 | Key string `json:"key"` 49 | Type string `json:"type"` 50 | Label string `json:"label"` 51 | Value string `json:"value"` 52 | Ttl string `json:"ttl"` 53 | } 54 | 55 | const ( 56 | // v1 API. 57 | URL = "https://register-api.did.id/v1/account/records" 58 | Key = "nextid" 59 | ) 60 | 61 | var ( 62 | l = logrus.WithFields(logrus.Fields{"module": "validator", "validator": "dotbit"}) 63 | ) 64 | 65 | func Init() { 66 | if validator.PlatformFactories == nil { 67 | validator.PlatformFactories = make(map[types.Platform]func(*validator.Base) validator.IValidator) 68 | } 69 | validator.PlatformFactories[types.Platforms.Das] = func(base *validator.Base) validator.IValidator { 70 | das := Das{base} 71 | return &das 72 | } 73 | } 74 | 75 | func (das *Das) GeneratePostPayload() (_ map[string]string) { 76 | return map[string]string{"default": "%COMPRESSED_PERSONA_PUBKEY_HEX%:%SIG_BASE64%"} 77 | } 78 | 79 | func (das *Das) GenerateSignPayload() (payload string) { 80 | das.Identity = strings.ToLower(das.Identity) 81 | payloadStruct := validator.H{ 82 | "action": string(das.Action), 83 | "identity": strings.ToLower(das.Identity), 84 | "platform": string(types.Platforms.Das), 85 | "prev": nil, 86 | "created_at": util.TimeToTimestampString(das.CreatedAt), 87 | "uuid": das.Uuid.String(), 88 | } 89 | if das.Previous != "" { 90 | payloadStruct["prev"] = das.Previous 91 | } 92 | 93 | payloadBytes, err := json.Marshal(payloadStruct) 94 | if err != nil { 95 | l.Warnf("Error when marshaling struct: %s", err.Error()) 96 | return "" 97 | } 98 | 99 | return string(payloadBytes) 100 | } 101 | 102 | func (das *Das) Validate() (err error) { 103 | das.Identity = strings.ToLower(das.Identity) 104 | das.AltID = das.Identity 105 | das.SignaturePayload = das.GenerateSignPayload() 106 | 107 | // Find the record through API response instead of saving its 'location'. 108 | req, err := json.Marshal(DasRequest{Account: das.Identity}) 109 | if err != nil { 110 | return xerrors.Errorf("Error when marshalling request: %w", err) 111 | } 112 | 113 | resp, err := http.Post(URL, "application/json", bytes.NewReader(req)) 114 | if err != nil { 115 | return xerrors.Errorf("Error when requesting proof: %s", err.Error()) 116 | } 117 | if resp.StatusCode != 200 { 118 | return xerrors.Errorf("Error when requesting proof: Status code %d", resp.StatusCode) 119 | } 120 | 121 | body, err := io.ReadAll(resp.Body) 122 | if err != nil { 123 | return xerrors.New("Error when getting resp body") 124 | } 125 | 126 | result := new(DasResponse) 127 | err = json.Unmarshal(body, result) 128 | if err != nil { 129 | return xerrors.Errorf("error when decoding JSON: %w", err) 130 | } 131 | 132 | return das.validateRecord(result) 133 | } 134 | 135 | func (das *Das) GetAltID() string { 136 | return das.AltID 137 | } 138 | 139 | func (das *Das) validateRecord(resp *DasResponse) error { 140 | if resp.ErrorNumber != 0 { 141 | return xerrors.Errorf("err_no %d: %s", resp.ErrorNumber, resp.ErrorMessage) 142 | } 143 | // Colon as the separator between public key and signature. 144 | valuePrefix := "0x" + mycrypto.CompressedPubkeyHex(das.Pubkey) + ":" 145 | record, ok := lo.Find(resp.Data.Records, func(i DasRecord) bool { 146 | // Find the first record starts with the public key. 147 | return i.Key == Key && i.Type == "profile" && strings.HasPrefix(i.Value, valuePrefix) 148 | }) 149 | if !ok { 150 | return xerrors.New("no key found") 151 | } 152 | 153 | _, sig, found := strings.Cut(record.Value, ":") 154 | if !found || len(sig) == 0 { 155 | return xerrors.New("invalid record value") 156 | } 157 | 158 | sigBytes, err := util.DecodeString(sig) 159 | if err != nil { 160 | return xerrors.Errorf("error when decoding sig: %w", err) 161 | } 162 | 163 | das.Signature = sigBytes 164 | return mycrypto.ValidatePersonalSignature(das.SignaturePayload, sigBytes, das.Pubkey) 165 | } 166 | --------------------------------------------------------------------------------