From 4876860f5401f7837875954ff31c9d7aa5817854 Mon Sep 17 00:00:00 2001 From: daveroga Date: Fri, 26 Jul 2024 16:28:55 +0200 Subject: [PATCH] add signature to resolver by parameter --- .golangci.yml | 10 +- Dockerfile | 2 +- README.md | 2 + cmd/driver/main.go | 2 +- go.mod | 4 + go.sum | 12 + pkg/app/configs/driver.go | 1 + pkg/app/handler.go | 11 +- pkg/document/did.go | 20 +- pkg/document/proof.go | 61 ++++ pkg/document/proof_test.go | 137 ++++++++ pkg/services/blockchain/eth/resolver.go | 342 ++++++++++++++++++- pkg/services/blockchain/eth/resolver_test.go | 175 +++++++++- pkg/services/did.go | 72 +++- pkg/services/registry.go | 11 + 15 files changed, 815 insertions(+), 47 deletions(-) create mode 100644 pkg/document/proof.go create mode 100644 pkg/document/proof_test.go diff --git a/.golangci.yml b/.golangci.yml index c547f45..96a41b5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,22 +39,16 @@ linters-settings: linters: enable: - bodyclose - - megacheck - revive - govet - unconvert - - megacheck - - structcheck - - gas + - gosec - gocyclo - dupl - misspell - unparam - - varcheck - - deadcode - typecheck - ineffassign - - varcheck - stylecheck - gochecknoinits - exportloopref @@ -66,6 +60,8 @@ linters: - errcheck - gofmt - goimports + - staticcheck + - unused fast: false disable-all: true diff --git a/Dockerfile b/Dockerfile index 80d0165..130d0d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ## ## Build did driver ## -FROM golang:1.18-alpine as base +FROM golang:1.18-alpine AS base WORKDIR /build diff --git a/README.md b/README.md index 726d57d..84f3516 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ Driver for the iden3 DID method amoy: contractAddress: "0xf6..." networkURL: "https://polygon-amoy..." + walletKey: "" ``` + `walletKey` is only needed for the resolver if it's a trusted resolver that includes signature of EIP712 message when requested in the resolution with `signature=EthereumEip712Signature2021`. 2. Build docker container: ```bash docker build -t driver-did-iden3:local diff --git a/cmd/driver/main.go b/cmd/driver/main.go index e697237..216c53b 100644 --- a/cmd/driver/main.go +++ b/cmd/driver/main.go @@ -63,7 +63,7 @@ func initResolvers() *services.ResolverRegistry { for chainName, chainSettings := range rs { for networkName, networkSettings := range chainSettings { prefix := fmt.Sprintf("%s:%s", chainName, networkName) - resolver, err := eth.NewResolver(networkSettings.NetworkURL, networkSettings.ContractAddress) + resolver, err := eth.NewResolver(networkSettings.NetworkURL, networkSettings.ContractAddress, networkSettings.WalletKey) if err != nil { log.Fatalf("failed configure resolver for network '%s': %v", prefix, err) } diff --git a/go.mod b/go.mod index 2aabf24..4f10990 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( ) require ( + github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect + github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/dchest/blake512 v1.0.0 // indirect github.com/deckarep/golang-set/v2 v2.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -55,6 +57,8 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect + github.com/tyler-smith/go-bip32 v1.0.0 + github.com/tyler-smith/go-bip39 v1.1.0 github.com/wealdtech/go-multicodec v1.4.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index e0b8dc9..eb756b6 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,8 @@ github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -20,6 +24,7 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoGMWEhDvS3zToKcDpRsLuRolQJBVGdozk= @@ -147,6 +152,7 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -155,7 +161,10 @@ github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+Kd github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= +github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa h1:5SqCsI/2Qya2bCzK15ozrqo2sZxkh0FHynJZOTVoV6Q= github.com/wealdtech/go-ens/v3 v3.5.5 h1:/jq3CDItK0AsFnZtiFJK44JthkAMD5YE3WAJOh4i7lc= github.com/wealdtech/go-ens/v3 v3.5.5/go.mod h1:w0EDKIm0dIQnqEKls6ORat/or+AVfPEdEXVfN71EeEE= @@ -166,11 +175,13 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= @@ -220,5 +231,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= diff --git a/pkg/app/configs/driver.go b/pkg/app/configs/driver.go index 42c1479..e3be040 100644 --- a/pkg/app/configs/driver.go +++ b/pkg/app/configs/driver.go @@ -16,6 +16,7 @@ const defaultPathToResolverSettings = "./resolvers.settings.yaml" type ResolverSettings map[string]map[string]struct { ContractAddress string `yaml:"contractAddress"` NetworkURL string `yaml:"networkURL"` + WalletKey string `yaml:"walletKey"` } // Config structure represent yaml config for did driver. diff --git a/pkg/app/handler.go b/pkg/app/handler.go index cc8b45c..bb34000 100644 --- a/pkg/app/handler.go +++ b/pkg/app/handler.go @@ -23,6 +23,7 @@ func (d *DidDocumentHandler) Get(w http.ResponseWriter, r *http.Request) { opts, err := getResolverOpts( r.URL.Query().Get("state"), r.URL.Query().Get("gist"), + r.URL.Query().Get("signature"), ) if err != nil { log.Println("invalid options query:", err) @@ -96,7 +97,7 @@ func (d *DidDocumentHandler) GetGist(w http.ResponseWriter, r *http.Request) { gistInfo, err := d.DidDocumentService.GetGist(r.Context(), chain, networkid, nil) if errors.Is(err, services.ErrNetworkIsNotSupported) { w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, `{"error":"resolver for '%s:%s' network not found"}`, chain, networkid) + log.Printf(`{"error":"resolver for '%s:%s' network not found"}`, chain, networkid) return } else if err != nil { log.Printf("failed get info about latest gist from network '%s:%s': %v\n", chain, networkid, err) @@ -110,7 +111,7 @@ func (d *DidDocumentHandler) GetGist(w http.ResponseWriter, r *http.Request) { } } -func getResolverOpts(state, gistRoot string) (ro services.ResolverOpts, err error) { +func getResolverOpts(state, gistRoot, signature string) (ro services.ResolverOpts, err error) { if state != "" && gistRoot != "" { return ro, errors.New("'state' and 'gist root' cannot be used together") } @@ -128,5 +129,11 @@ func getResolverOpts(state, gistRoot string) (ro services.ResolverOpts, err erro } ro.GistRoot = g.BigInt() } + if signature != "" { + if signature != "EthereumEip712Signature2021" { + return ro, fmt.Errorf("not supported signature type %s", signature) + } + ro.Signature = signature + } return } diff --git a/pkg/document/did.go b/pkg/document/did.go index 715e305..c4154c7 100644 --- a/pkg/document/did.go +++ b/pkg/document/did.go @@ -15,6 +15,7 @@ const ( ErrUnknownNetwork ErrorCode = "unknownNetwork" StateType = "Iden3StateInfo2023" + Iden3ResolutionMetadataType = "Iden3ResolutionMetadata" EcdsaSecp256k1RecoveryMethod2020Type = "EcdsaSecp256k1RecoveryMethod2020" ) @@ -24,6 +25,8 @@ const ( iden3Context = "https://schema.iden3.io/core/jsonld/auth.jsonld" EcdsaSecp256k1RecoveryContext = "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020/lds-ecdsa-secp256k1-recovery2020-2.0.jsonld" defaultContentType = "application/did+ld+json" + iden3ResolutionContext = "https://schema.iden3.io/core/jsonld/resolution.jsonld" + eip712sigContext = "https://w3id.org/security/suites/eip712sig-2021/v1" ) // DidResolution representation of did resolution. @@ -45,6 +48,8 @@ func NewDidResolution() *DidResolution { VerificationMethod: []verifiable.CommonVerificationMethod{}, }, DidResolutionMetadata: &DidResolutionMetadata{ + Context: []string{iden3ResolutionContext}, + Type: Iden3ResolutionMetadataType, ContentType: defaultContentType, Retrieved: time.Now(), }, @@ -52,6 +57,10 @@ func NewDidResolution() *DidResolution { } } +func DidResolutionMetadataSigContext() []string { + return []string{iden3ResolutionContext, eip712sigContext} +} + func NewDidMethodNotSupportedResolution(msg string) *DidResolution { return NewDidErrorResolution(ErrMethodNotSupported, msg) } @@ -81,10 +90,13 @@ func NewDidErrorResolution(errCode ErrorCode, errMsg string) *DidResolution { // DidResolutionMetadata representation of resolution metadata. type DidResolutionMetadata struct { - Error ErrorCode `json:"error,omitempty"` - Message string `json:"message,omitempty"` - ContentType string `json:"contentType,omitempty"` - Retrieved time.Time `json:"retrieved,omitempty"` + Context []string `json:"@context,omitempty"` + Error ErrorCode `json:"error,omitempty"` + Message string `json:"message,omitempty"` + ContentType string `json:"contentType,omitempty"` + Retrieved time.Time `json:"retrieved,omitempty"` + Type string `json:"type,omitempty"` + Proof DidResolutionProofs `json:"proof,omitempty"` } // DidDocumentMetadata metadata of did document. diff --git a/pkg/document/proof.go b/pkg/document/proof.go new file mode 100644 index 0000000..c080932 --- /dev/null +++ b/pkg/document/proof.go @@ -0,0 +1,61 @@ +package document + +import ( + "encoding/json" + "errors" + "time" + + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/iden3/go-schema-processor/v2/verifiable" +) + +type DidResolutionProof interface { + ProofType() verifiable.ProofType +} + +type DidResolutionProofs []DidResolutionProof + +type EthereumEip712SignatureProof2021 struct { + Type verifiable.ProofType `json:"type"` + ProofPursopose string `json:"proofPurpose"` + ProofValue string `json:"proofValue"` + VerificationMethod string `json:"verificationMethod"` + Created time.Time `json:"created"` + Eip712 apitypes.TypedData `json:"eip712"` +} + +// EthereumEip712Signature2021Type is a proof type for EIP172 signature proofs +// nolint:stylecheck // we need to keep the name as it is +const EthereumEip712SignatureProof2021Type verifiable.ProofType = "EthereumEip712Signature2021" + +func (p *EthereumEip712SignatureProof2021) ProofType() verifiable.ProofType { + return p.Type +} + +func (p *EthereumEip712SignatureProof2021) UnmarshalJSON(in []byte) error { + var obj struct { + Type verifiable.ProofType `json:"type"` + ProofPursopose string `json:"proofPurpose"` + ProofValue string `json:"proofValue"` + VerificationMethod string `json:"verificationMethod"` + Created time.Time `json:"created"` + Eip712 json.RawMessage `json:"eip712"` + } + err := json.Unmarshal(in, &obj) + if err != nil { + return err + } + if obj.Type != EthereumEip712SignatureProof2021Type { + return errors.New("invalid proof type") + } + p.Type = obj.Type + err = json.Unmarshal(obj.Eip712, &p.Eip712) + if err != nil { + return err + } + p.VerificationMethod = obj.VerificationMethod + p.ProofPursopose = obj.ProofPursopose + p.ProofValue = obj.ProofValue + p.Created = obj.Created + return nil +} diff --git a/pkg/document/proof_test.go b/pkg/document/proof_test.go new file mode 100644 index 0000000..90f19d1 --- /dev/null +++ b/pkg/document/proof_test.go @@ -0,0 +1,137 @@ +package document + +import ( + "encoding/json" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/stretchr/testify/require" +) + +func TestEthereumEip712SignatureProof2021_JSONUnmarshal(t *testing.T) { + in := `{ + "type": "EthereumEip712Signature2021", + "proofPurpose": "assertionMethod", + "proofValue": "0xd5e5ffe290a258116a0f7acb4c9a5bbfdd842516061c6a794892b6db05fbd14706de7e189d965bead2ffb23e30d2f6b02ecf764e6fe24be788721049b7e331481c", + "verificationMethod": "did:pkh:eip155:1:0x5b18eF56aA61eeAE0E3434e3c3d8AEB19b141fe7#blockchainAccountId", + "created": "2021-09-23T20:21:34Z", + "eip712": { + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "IdentityState": [ + { "name": "from", "type": "address" }, + { "name": "timestamp", "type": "uint256" }, + { "name": "state", "type": "uint256" }, + { "name": "stateCreatedAtTimestamp", "type": "uint256" }, + { "name": "stateReplacedByState", "type": "uint256" }, + { "name": "stateReplacedAtTimestamp", "type": "uint256" }, + { "name": "gistRoot", "type": "uint256" }, + { "name": "gistRootCreatedAtTimestamp", "type": "uint256" }, + { "name": "gistRootReplacedByRoot", "type": "uint256" }, + { "name": "gistRootReplacedAtTimestamp", "type": "uint256" }, + { "name": "identity", "type": "uint256" } + ] + }, + "primaryType": "IdentityState", + "domain": { + "name": "StateInfo", + "version": "1", + "chainId": "0x1", + "verifyingContract": "0x0000000000000000000000000000000000000000" + }, + "message": { + "from": "0x5b18eF56aA61eeAE0E3434e3c3d8AEB19b141fe7", + "timestamp": "0", + "state": "444", + "stateCreatedAtTimestamp": "0", + "stateReplacedByState": "0", + "stateReplacedAtTimestamp": "0", + "gistRoot": "555", + "gistRootCreatedAtTimestamp": "0", + "gistRootReplacedByRoot": "0", + "gistRootReplacedAtTimestamp": "0", + "identity": "19090607534999372304474213543962416547920895595808567155882840509226423042" + } + } + }` + var proof EthereumEip712SignatureProof2021 + err := json.Unmarshal([]byte(in), &proof) + require.NoError(t, err) + + timeParsed, _ := time.Parse("2006-01-02T15:04:05Z", "2021-09-23T20:21:34Z") + + var apiTypes = apitypes.Types{ + "IdentityState": []apitypes.Type{ + {Name: "from", Type: "address"}, + {Name: "timestamp", Type: "uint256"}, + {Name: "state", Type: "uint256"}, + {Name: "stateCreatedAtTimestamp", Type: "uint256"}, + {Name: "stateReplacedByState", Type: "uint256"}, + {Name: "stateReplacedAtTimestamp", Type: "uint256"}, + {Name: "gistRoot", Type: "uint256"}, + {Name: "gistRootCreatedAtTimestamp", Type: "uint256"}, + {Name: "gistRootReplacedByRoot", Type: "uint256"}, + {Name: "gistRootReplacedAtTimestamp", Type: "uint256"}, + {Name: "identity", Type: "uint256"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + } + + var primaryType = "IdentityState" + walletAddress := "0x5b18eF56aA61eeAE0E3434e3c3d8AEB19b141fe7" + state := "444" + stateCreatedAtTimestamp := "0" + stateReplacedByState := "0" + stateReplacedAtTimestamp := "0" + gistRootCreatedAtTimestamp := "0" + gistRootReplacedByRoot := "0" + gistRootReplacedAtTimestamp := "0" + timestamp := "0" + gistRoot := "555" + identity := "19090607534999372304474213543962416547920895595808567155882840509226423042" + chainID := 1 + + wantProof := EthereumEip712SignatureProof2021{ + Type: "EthereumEip712Signature2021", + ProofPursopose: "assertionMethod", + ProofValue: "0xd5e5ffe290a258116a0f7acb4c9a5bbfdd842516061c6a794892b6db05fbd14706de7e189d965bead2ffb23e30d2f6b02ecf764e6fe24be788721049b7e331481c", + VerificationMethod: "did:pkh:eip155:1:0x5b18eF56aA61eeAE0E3434e3c3d8AEB19b141fe7#blockchainAccountId", + Created: timeParsed, + Eip712: apitypes.TypedData{ + Types: apiTypes, + PrimaryType: primaryType, + Domain: apitypes.TypedDataDomain{ + Name: "StateInfo", + Version: "1", + ChainId: math.NewHexOrDecimal256(int64(chainID)), + VerifyingContract: "0x0000000000000000000000000000000000000000", + }, + Message: apitypes.TypedDataMessage{ + "from": walletAddress, + "timestamp": timestamp, + "state": state, + "stateCreatedAtTimestamp": stateCreatedAtTimestamp, + "stateReplacedByState": stateReplacedByState, + "stateReplacedAtTimestamp": stateReplacedAtTimestamp, + "gistRoot": gistRoot, + "gistRootCreatedAtTimestamp": gistRootCreatedAtTimestamp, + "gistRootReplacedByRoot": gistRootReplacedByRoot, + "gistRootReplacedAtTimestamp": gistRootReplacedAtTimestamp, + "identity": identity, + }, + }, + } + require.Equal(t, wantProof, proof) +} diff --git a/pkg/services/blockchain/eth/resolver.go b/pkg/services/blockchain/eth/resolver.go index 6210c3c..dd4ba24 100644 --- a/pkg/services/blockchain/eth/resolver.go +++ b/pkg/services/blockchain/eth/resolver.go @@ -2,13 +2,22 @@ package eth import ( "context" + "crypto/ecdsa" + "crypto/subtle" + "encoding/hex" "errors" "fmt" "math/big" + "strconv" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/signer/core/apitypes" contract "github.com/iden3/contracts-abi/state/go/abi" "github.com/iden3/driver-did-iden3/pkg/services" core "github.com/iden3/go-iden3-core/v2" @@ -30,6 +39,13 @@ type Resolver struct { contractAddress string chainID int + walletKey string +} + +type AuthData struct { + TypedData apitypes.TypedData + Signature string + Address string } var ( @@ -38,8 +54,45 @@ var ( stateNotFoundException = "execution reverted: State does not exist" ) +var IdentityStateAPITypes = apitypes.Types{ + "IdentityState": []apitypes.Type{ + {Name: "from", Type: "address"}, + {Name: "timestamp", Type: "uint256"}, + {Name: "identity", Type: "uint256"}, + {Name: "state", Type: "uint256"}, + {Name: "replacedByState", Type: "uint256"}, + {Name: "createdAtTimestamp", Type: "uint256"}, + {Name: "replacedAtTimestamp", Type: "uint256"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, +} + +var GlobalStateAPITypes = apitypes.Types{ + "GlobalState": []apitypes.Type{ + {Name: "from", Type: "address"}, + {Name: "timestamp", Type: "uint256"}, + {Name: "root", Type: "uint256"}, + {Name: "replacedByRoot", Type: "uint256"}, + {Name: "createdAtTimestamp", Type: "uint256"}, + {Name: "replacedAtTimestamp", Type: "uint256"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, +} + +var TimeStamp = TimeStampFn + // NewResolver create new ethereum resolver. -func NewResolver(url, address string) (*Resolver, error) { +func NewResolver(url, address, walletKey string) (*Resolver, error) { c, err := ethclient.Dial(url) if err != nil { return nil, err @@ -52,6 +105,7 @@ func NewResolver(url, address string) (*Resolver, error) { resolver := &Resolver{ state: sc, contractAddress: address, + walletKey: walletKey, } chainID, err := c.NetworkID(context.Background()) if err != nil { @@ -65,6 +119,27 @@ func (r *Resolver) BlockchainID() string { return fmt.Sprintf("%d:%s", r.chainID, r.contractAddress) } +func (r *Resolver) WalletAddress() (string, error) { + if r.walletKey == "" { + return "", errors.New("wallet key is not set") + } + + privateKey, err := crypto.HexToECDSA(r.walletKey) + if err != nil { + return "", err + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return "", errors.New("error casting public key to ECDSA") + } + + walletAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + return walletAddress.String(), nil +} + func (r *Resolver) ResolveGist( ctx context.Context, opts *services.ResolverOpts, @@ -110,19 +185,31 @@ func (r *Resolver) Resolve( err error ) - userID, err := core.IDFromDID(did) - if err != nil { - return services.IdentityState{}, - fmt.Errorf("invalid did format for did '%s': %v", did, err) - } + if did.IDStrings[2] == "000000000000000000000000000000000000000000" { + if opts.GistRoot == nil { + return services.IdentityState{}, + errors.New("options GistRoot is required for root only did") + } + stateInfo = nil + gistInfo, err = r.resolveGistRootOnly(ctx, opts.GistRoot) + } else { + userID, err := core.IDFromDID(did) + if err != nil { + return services.IdentityState{}, + fmt.Errorf("invalid did format for did '%s': %v", did, err) + } - switch { - case opts.GistRoot != nil: - stateInfo, gistInfo, err = r.resolveStateByGistRoot(ctx, userID, opts.GistRoot) - case opts.State != nil: - stateInfo, err = r.resolveState(ctx, userID, opts.State) - default: - stateInfo, gistInfo, err = r.resolveLatest(ctx, userID) + switch { + case opts.GistRoot != nil: + stateInfo, gistInfo, err = r.resolveStateByGistRoot(ctx, userID, opts.GistRoot) + case opts.State != nil: + stateInfo, err = r.resolveState(ctx, userID, opts.State) + default: + stateInfo, gistInfo, err = r.resolveLatest(ctx, userID) + } + if err != nil && err.Error() != "identity not found" { + return services.IdentityState{}, err + } } identityState := services.IdentityState{} @@ -148,9 +235,226 @@ func (r *Resolver) Resolve( } } + signature := "" + if r.walletKey != "" && opts.Signature != "" { + primaryType := services.IdentityStateType + if stateInfo == nil { + primaryType = services.GlobalStateType + } + signature, err = r.signTypedData(primaryType, did, identityState) + if err != nil { + return services.IdentityState{}, err + } + } + + identityState.Signature = signature + return identityState, err } +func (r *Resolver) VerifyState( + primaryType services.PrimaryType, + identityState services.IdentityState, + did w3c.DID, +) (bool, error) { + privateKey, err := crypto.HexToECDSA(r.walletKey) + if err != nil { + return false, err + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return false, errors.New("error casting public key to ECDSA") + } + + walletAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + typedData, err := r.TypedData(primaryType, did, identityState, walletAddress.String()) + if err != nil { + return false, err + } + + authData := AuthData{TypedData: typedData, Signature: identityState.Signature, Address: walletAddress.String()} + return r.verifyTypedData(authData) +} + +func TimeStampFn() string { + timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10) + return timestamp +} + +func (r *Resolver) TypedData(primaryType services.PrimaryType, did w3c.DID, identityState services.IdentityState, walletAddress string) (apitypes.TypedData, error) { + identity := "0" + if did.IDStrings[2] != "000000000000000000000000000000000000000000" { + userID, err := core.IDFromDID(did) + if err != nil { + return apitypes.TypedData{}, + fmt.Errorf("invalid did format for did '%s': %v", did, err) + } + identity = userID.BigInt().String() + } + + root := "0" + state := "0" + createdAtTimestamp := "0" + replacedByRoot := "0" + replacedByState := "0" + replacedAtTimestamp := "0" + + if identityState.StateInfo != nil { + state = identityState.StateInfo.State.String() + replacedByState = identityState.StateInfo.ReplacedByState.String() + createdAtTimestamp = identityState.StateInfo.CreatedAtTimestamp.String() + replacedAtTimestamp = identityState.StateInfo.ReplacedAtTimestamp.String() + } + if identityState.GistInfo != nil { + root = identityState.GistInfo.Root.String() + replacedByRoot = identityState.GistInfo.ReplacedByRoot.String() + createdAtTimestamp = identityState.GistInfo.CreatedAtTimestamp.String() + replacedAtTimestamp = identityState.GistInfo.ReplacedAtTimestamp.String() + } + + apiTypes := apitypes.Types{} + message := apitypes.TypedDataMessage{} + primaryTypeString := "" + timestamp := TimeStamp() + + switch primaryType { + case services.IdentityStateType: + primaryTypeString = "IdentityState" + apiTypes = IdentityStateAPITypes + message = apitypes.TypedDataMessage{ + "from": walletAddress, + "timestamp": timestamp, + "identity": identity, + "state": state, + "replacedByState": replacedByState, + "createdAtTimestamp": createdAtTimestamp, + "replacedAtTimestamp": replacedAtTimestamp, + } + case services.GlobalStateType: + primaryTypeString = "GlobalState" + apiTypes = GlobalStateAPITypes + message = apitypes.TypedDataMessage{ + "from": walletAddress, + "timestamp": timestamp, + "root": root, + "replacedByRoot": replacedByRoot, + "createdAtTimestamp": createdAtTimestamp, + "replacedAtTimestamp": replacedAtTimestamp, + } + } + + typedData := apitypes.TypedData{ + Types: apiTypes, + PrimaryType: primaryTypeString, + Domain: apitypes.TypedDataDomain{ + Name: "StateInfo", + Version: "1", + ChainId: math.NewHexOrDecimal256(int64(0)), + VerifyingContract: "0x0000000000000000000000000000000000000000", + }, + Message: message, + } + + return typedData, nil +} + +func (r *Resolver) signTypedData(primaryType services.PrimaryType, did w3c.DID, identityState services.IdentityState) (string, error) { + privateKey, err := crypto.HexToECDSA(r.walletKey) + if err != nil { + return "", err + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return "", errors.New("error casting public key to ECDSA") + } + + walletAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + typedData, err := r.TypedData(primaryType, did, identityState, walletAddress.String()) + if err != nil { + return "", errors.New("error getting typed data for signing") + } + + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return "", errors.New("error hashing EIP712Domain for signing") + } + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return "", errors.New("error hashing PrimaryType message for signing") + } + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) + dataHash := crypto.Keccak256(rawData) + + signature, err := crypto.Sign(dataHash, privateKey) + if err != nil { + return "", err + } + + if signature[64] < 27 { + signature[64] += 27 + } + + return "0x" + hex.EncodeToString(signature), nil +} + +func (r *Resolver) verifyTypedData(authData AuthData) (bool, error) { + signature, err := hexutil.Decode(authData.Signature) + if err != nil { + return false, fmt.Errorf("decode signature: %w", err) + } + + // EIP-712 typed data marshaling + domainSeparator, err := authData.TypedData.HashStruct("EIP712Domain", authData.TypedData.Domain.Map()) + if err != nil { + return false, fmt.Errorf("eip712domain hash struct: %w", err) + } + typedDataHash, err := authData.TypedData.HashStruct(authData.TypedData.PrimaryType, authData.TypedData.Message) + if err != nil { + return false, fmt.Errorf("primary type hash struct: %w", err) + } + + // add magic string prefix + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) + sighash := crypto.Keccak256(rawData) + + // update the recovery id + // https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442 + signature[64] -= 27 + + // get the pubkey used to sign this signature + sigPubkey, err := crypto.Ecrecover(sighash, signature) + if err != nil { + return false, fmt.Errorf("ecrecover: %w", err) + } + + // get the address to confirm it's the same one in the auth token + pubkey, err := crypto.UnmarshalPubkey(sigPubkey) + if err != nil { + return false, fmt.Errorf("unmarshal pub key: %w", err) + } + address := crypto.PubkeyToAddress(*pubkey) + + // verify the signature (not sure if this is actually required after ecrecover) + signatureNoRecoverID := signature[:len(signature)-1] + verified := crypto.VerifySignature(sigPubkey, sighash, signatureNoRecoverID) + if !verified { + return false, errors.New("verification failed") + } + + dataAddress := common.HexToAddress(authData.Address) + if subtle.ConstantTimeCompare(address.Bytes(), dataAddress.Bytes()) == 0 { + return false, errors.New("address mismatch") + } + + return true, nil +} + func (r *Resolver) resolveLatest( ctx context.Context, id core.ID, @@ -172,6 +476,18 @@ func (r *Resolver) resolveLatest( return &stateInfo, &gistInfo, verifyContractState(id, stateInfo) } +func (r *Resolver) resolveGistRootOnly( + ctx context.Context, + gistRoot *big.Int, +) (*contract.IStateGistRootInfo, error) { + gistInfo, err := r.state.GetGISTRootInfo(&bind.CallOpts{Context: ctx}, gistRoot) + if err = notFoundErr(err); err != nil { + return nil, err + } + + return &gistInfo, nil +} + func (r *Resolver) resolveState( ctx context.Context, id core.ID, diff --git a/pkg/services/blockchain/eth/resolver_test.go b/pkg/services/blockchain/eth/resolver_test.go index 89fa004..150c9cf 100644 --- a/pkg/services/blockchain/eth/resolver_test.go +++ b/pkg/services/blockchain/eth/resolver_test.go @@ -6,6 +6,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/crypto" "github.com/golang/mock/gomock" contract "github.com/iden3/contracts-abi/state/go/abi" "github.com/iden3/driver-did-iden3/pkg/services" @@ -14,9 +15,11 @@ import ( "github.com/iden3/go-iden3-core/v2/w3c" "github.com/pkg/errors" "github.com/stretchr/testify/require" + "github.com/tyler-smith/go-bip32" + "github.com/tyler-smith/go-bip39" ) -var userDID, _ = w3c.ParseDID("did:polygonid:polygon:mumbai:2qJEaVmT5jBrtgBQ4m7b7bRYzWmvMyDjBZGP24QwvD") +var userDID, _ = w3c.ParseDID("did:polygonid:polygon:amoy:2qY71pSkdCsRetTHbUA4YqG7Hx63Ej2PeiJMzAdJ2V") func TestResolveGist_Success(t *testing.T) { tests := []struct { @@ -201,3 +204,173 @@ func TestNotFoundErr(t *testing.T) { }) } } + +func TestResolveSignature_Success(t *testing.T) { + userEmptyDID, _ := w3c.ParseDID("did:polygonid:polygon:amoy:000000000000000000000000000000000000000000") + + tests := []struct { + name string + opts *services.ResolverOpts + userDID *w3c.DID + contractMock func(c *cm.MockStateContract) + timeStamp func() string + expectedIdentityState services.IdentityState + }{ + { + name: "resolve identity state by gist", + opts: &services.ResolverOpts{ + GistRoot: big.NewInt(1), + Signature: "EthereumEip712Signature2021", + }, + userDID: userDID, + contractMock: func(c *cm.MockStateContract) { + proof := contract.IStateGistProof{ + Root: big.NewInt(4), + Existence: true, + Value: big.NewInt(5), + } + userID, _ := core.IDFromDID(*userDID) + c.EXPECT().GetGISTProofByRoot(gomock.Any(), userID.BigInt(), big.NewInt(1)).Return(proof, nil) + gistInfo := contract.IStateGistRootInfo{Root: big.NewInt(555), CreatedAtTimestamp: big.NewInt(0), ReplacedByRoot: big.NewInt(0), ReplacedAtTimestamp: big.NewInt(0)} + c.EXPECT().GetGISTRootInfo(gomock.Any(), big.NewInt(4)).Return(gistInfo, nil) + stateInfo := contract.IStateStateInfo{Id: userID.BigInt(), State: big.NewInt(444), CreatedAtTimestamp: big.NewInt(0), ReplacedByState: big.NewInt(0), ReplacedAtTimestamp: big.NewInt(0)} + c.EXPECT().GetStateInfoByIdAndState(gomock.Any(), gomock.Any(), big.NewInt(5)).Return(stateInfo, nil) + }, + timeStamp: func() string { + return "0" + }, + expectedIdentityState: services.IdentityState{ + StateInfo: &services.StateInfo{ + ID: *userDID, + State: big.NewInt(444), + CreatedAtTimestamp: big.NewInt(0), + ReplacedByState: big.NewInt(0), + ReplacedAtTimestamp: big.NewInt(0), + }, + GistInfo: &services.GistInfo{ + Root: big.NewInt(555), + CreatedAtTimestamp: big.NewInt(0), + ReplacedByRoot: big.NewInt(0), + ReplacedAtTimestamp: big.NewInt(0), + }, + Signature: "0x6276946bac246584ed6eaa2d5e43be5147e67cc7aa3b969c82bb9b1670e8de8b7f7410286f25d6bee4330b4bc260286cf8505358ffa29c8e677e4f05d78acf131c", + }, + }, + { + name: "resolve identity state by state", + opts: &services.ResolverOpts{ + State: big.NewInt(1), + Signature: "EthereumEip712Signature2021", + }, + userDID: userDID, + contractMock: func(c *cm.MockStateContract) { + userID, _ := core.IDFromDID(*userDID) + res := contract.IStateStateInfo{Id: userID.BigInt(), State: big.NewInt(555), CreatedAtTimestamp: big.NewInt(0), ReplacedByState: big.NewInt(0), ReplacedAtTimestamp: big.NewInt(0)} + c.EXPECT().GetStateInfoByIdAndState(gomock.Any(), gomock.Any(), big.NewInt(1)).Return(res, nil) + }, + timeStamp: func() string { + return "0" + }, + expectedIdentityState: services.IdentityState{ + StateInfo: &services.StateInfo{ + ID: *userDID, + State: big.NewInt(555), + CreatedAtTimestamp: big.NewInt(0), + ReplacedByState: big.NewInt(0), + ReplacedAtTimestamp: big.NewInt(0), + }, + GistInfo: nil, + Signature: "0xdd07cd99ee8aa853c3e942aa5d57bfb844cae3db35fe29e8fc635ff4b2f5377d4b3c65f270474e6c5931b3d77f536233bc56d63172da8dba188f1f6fa51a10cb1c", + }, + }, + { + name: "resolve latest state", + opts: &services.ResolverOpts{ + Signature: "EthereumEip712Signature2021", + }, + userDID: userDID, + contractMock: func(c *cm.MockStateContract) { + userID, _ := core.IDFromDID(*userDID) + latestGist := big.NewInt(100) + c.EXPECT().GetGISTRoot(gomock.Any()).Return(latestGist, nil) + latestGistInfo := contract.IStateGistRootInfo{Root: big.NewInt(400), CreatedAtTimestamp: big.NewInt(0), ReplacedByRoot: big.NewInt(0), ReplacedAtTimestamp: big.NewInt(0)} + c.EXPECT().GetGISTRootInfo(gomock.Any(), latestGist).Return(latestGistInfo, nil) + stateInfo := contract.IStateStateInfo{Id: userID.BigInt(), State: big.NewInt(555), CreatedAtTimestamp: big.NewInt(0), ReplacedByState: big.NewInt(0), ReplacedAtTimestamp: big.NewInt(0)} + c.EXPECT().GetStateInfoById(gomock.Any(), userID.BigInt()).Return(stateInfo, nil) + }, + timeStamp: func() string { + return "0" + }, + expectedIdentityState: services.IdentityState{ + StateInfo: &services.StateInfo{ + ID: *userDID, + State: big.NewInt(555), + CreatedAtTimestamp: big.NewInt(0), + ReplacedByState: big.NewInt(0), + ReplacedAtTimestamp: big.NewInt(0), + }, + GistInfo: &services.GistInfo{ + Root: big.NewInt(400), + CreatedAtTimestamp: big.NewInt(0), + ReplacedByRoot: big.NewInt(0), + ReplacedAtTimestamp: big.NewInt(0), + }, + Signature: "0xdd07cd99ee8aa853c3e942aa5d57bfb844cae3db35fe29e8fc635ff4b2f5377d4b3c65f270474e6c5931b3d77f536233bc56d63172da8dba188f1f6fa51a10cb1c", + }, + }, + { + name: "resolve only gist", + opts: &services.ResolverOpts{ + GistRoot: big.NewInt(400), + Signature: "EthereumEip712Signature2021", + }, + userDID: userEmptyDID, + contractMock: func(c *cm.MockStateContract) { + latestGist := big.NewInt(400) + latestGistInfo := contract.IStateGistRootInfo{Root: big.NewInt(400), CreatedAtTimestamp: big.NewInt(0), ReplacedByRoot: big.NewInt(0), ReplacedAtTimestamp: big.NewInt(0)} + c.EXPECT().GetGISTRootInfo(gomock.Any(), latestGist).Return(latestGistInfo, nil) + }, + timeStamp: func() string { + return "0" + }, + expectedIdentityState: services.IdentityState{ + StateInfo: nil, + GistInfo: &services.GistInfo{ + Root: big.NewInt(400), + CreatedAtTimestamp: big.NewInt(0), + ReplacedByRoot: big.NewInt(0), + ReplacedAtTimestamp: big.NewInt(0), + }, + Signature: "0xe64e080d08b948e5303b49288f1ff599df5b21fd20d7a944026a17e69f860e21662538ec1f8cba2f4a76e7c25d0f5cf506dc16bbc3148158ed81dd899528c69f1c", + }, + }, + } + + mnemonic := "rib satisfy drastic trigger trial exclude raccoon wedding then gaze fire hero" + seed := bip39.NewSeed(mnemonic, "Secret Passphrase bla bla bla") + masterPrivateKey, _ := bip32.NewMasterKey(seed) + ecdaPrivateKey := crypto.ToECDSAUnsafe(masterPrivateKey.Key) + privateKeyHex := fmt.Sprintf("%x", ecdaPrivateKey.D) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + stateContract := cm.NewMockStateContract(ctrl) + tt.contractMock(stateContract) + TimeStamp = tt.timeStamp + resolver := Resolver{state: stateContract, chainID: 1, walletKey: privateKeyHex} + identityState, err := resolver.Resolve(context.Background(), *tt.userDID, tt.opts) + require.NoError(t, err) + require.Equal(t, tt.expectedIdentityState, identityState) + + primaryType := services.IdentityStateType + if tt.expectedIdentityState.StateInfo == nil { + primaryType = services.GlobalStateType + } + + ok, _ := resolver.VerifyState(primaryType, identityState, *tt.userDID) + require.Equal(t, true, ok) + ctrl.Finish() + }) + } +} diff --git a/pkg/services/did.go b/pkg/services/did.go index d1d2e7f..fd86c7e 100644 --- a/pkg/services/did.go +++ b/pkg/services/did.go @@ -6,6 +6,7 @@ import ( "math/big" "net" "strings" + "time" "github.com/iden3/driver-did-iden3/pkg/document" "github.com/iden3/driver-did-iden3/pkg/services/ens" @@ -25,8 +26,9 @@ type DidDocumentServices struct { } type ResolverOpts struct { - State *big.Int - GistRoot *big.Int + State *big.Int + GistRoot *big.Int + Signature string } func NewDidDocumentServices(resolvers *ResolverRegistry, registry *ens.Registry) *DidDocumentServices { @@ -45,25 +47,35 @@ func (d *DidDocumentServices) GetDidDocument(ctx context.Context, did string, op return errResolution, err } - userID, err := core.IDFromDID(*userDID) - errResolution, err = expectedError(err) - if err != nil { - return errResolution, err - } + blockchain := "" + network := "" + userID := core.ID{} + if userDID.IDStrings[2] == "000000000000000000000000000000000000000000" { + blockchain = userDID.IDStrings[0] + network = userDID.IDStrings[1] + } else { + userID, err = core.IDFromDID(*userDID) + errResolution, err = expectedError(err) + if err != nil { + return errResolution, err + } - b, err := core.BlockchainFromID(userID) - errResolution, err = expectedError(err) - if err != nil { - return errResolution, err - } + b, err := core.BlockchainFromID(userID) + errResolution, err = expectedError(err) + if err != nil { + return errResolution, err + } - n, err := core.NetworkIDFromID(userID) - errResolution, err = expectedError(err) - if err != nil { - return errResolution, err + n, err := core.NetworkIDFromID(userID) + errResolution, err = expectedError(err) + if err != nil { + return errResolution, err + } + blockchain = string(b) + network = string(n) } - resolver, err := d.resolvers.GetResolverByNetwork(string(b), string(n)) + resolver, err := d.resolvers.GetResolverByNetwork(blockchain, network) errResolution, err = expectedError(err) if err != nil { return errResolution, err @@ -97,7 +109,7 @@ func (d *DidDocumentServices) GetDidDocument(ctx context.Context, did string, op chainIDStateAddress := resolver.BlockchainID() - if err == nil { + if err == nil && userDID.IDStrings[2] != "000000000000000000000000000000000000000000" { didResolution.DidDocument.Context = append(didResolution.DidDocument.Context.([]string), document.EcdsaSecp256k1RecoveryContext) addressString := fmt.Sprintf("%x", addr) blockchainAccountID := fmt.Sprintf("eip155:%s:0x%s", strings.Split(chainIDStateAddress, ":")[0], addressString) @@ -128,6 +140,30 @@ func (d *DidDocumentServices) GetDidDocument(ctx context.Context, did string, op }, ) + walletAddress, err := resolver.WalletAddress() + + if err == nil && opts.Signature != "" { + primaryType := IdentityStateType + if userDID.IDStrings[2] == "000000000000000000000000000000000000000000" { + primaryType = GlobalStateType + } + eip712TypedData, err := resolver.TypedData(primaryType, *userDID, identityState, walletAddress) + if err != nil { + return nil, fmt.Errorf("invalid typed data: %v", err) + } + + eip712Proof := &document.EthereumEip712SignatureProof2021{ + Type: document.EthereumEip712SignatureProof2021Type, + ProofPursopose: "assertionMethod", + ProofValue: identityState.Signature, + VerificationMethod: fmt.Sprintf("did:pkh:eip155:0:%s#blockchainAccountId", walletAddress), + Eip712: eip712TypedData, + Created: time.Now(), + } + + didResolution.DidResolutionMetadata.Context = document.DidResolutionMetadataSigContext() + didResolution.DidResolutionMetadata.Proof = append(didResolution.DidResolutionMetadata.Proof, eip712Proof) + } return didResolution, nil } diff --git a/pkg/services/registry.go b/pkg/services/registry.go index 86dc1d0..5e858a2 100644 --- a/pkg/services/registry.go +++ b/pkg/services/registry.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/iden3/go-iden3-core/v2/w3c" "github.com/iden3/go-merkletree-sql/v2" "github.com/iden3/go-schema-processor/v2/verifiable" @@ -18,9 +19,17 @@ var ( ErrNotFound = errors.New("not found") ) +type PrimaryType int32 + +const ( + IdentityStateType PrimaryType = 0 + GlobalStateType PrimaryType = 1 +) + type IdentityState struct { StateInfo *StateInfo GistInfo *GistInfo + Signature string } type StateInfo struct { @@ -95,6 +104,8 @@ type Resolver interface { Resolve(ctx context.Context, did w3c.DID, opts *ResolverOpts) (IdentityState, error) ResolveGist(ctx context.Context, opts *ResolverOpts) (*GistInfo, error) BlockchainID() string + WalletAddress() (string, error) + TypedData(primaryType PrimaryType, did w3c.DID, identityState IdentityState, walletAddress string) (apitypes.TypedData, error) } type ResolverRegistry map[string]Resolver