diff --git a/.gitignore b/.gitignore index bb777aa..a771297 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ # Envs configs/driver.yaml .env* -resolvers.settings.yaml +*.settings.yaml test.http diff --git a/Dockerfile b/Dockerfile index 130d0d0..3406dba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN CGO_ENABLED=0 go build -o ./driver ./cmd/driver/main.go # Build an driver image FROM scratch -COPY ./resolvers.settings.yaml /app/resolvers.settings.yaml +COPY ./*.settings.yaml /app/ COPY --from=base /build/driver /app/driver COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ @@ -28,4 +28,4 @@ ENV HOST=0.0.0.0 ENV PORT=8080 # Command to run -ENTRYPOINT ["/app/driver", "/app/resolvers.settings.yaml"] +ENTRYPOINT ["/app/driver", "/app/resolvers.settings.yaml", "/app/signers.settings.yaml"] diff --git a/cmd/driver/main.go b/cmd/driver/main.go index 9c5ae4c..7a19b9e 100644 --- a/cmd/driver/main.go +++ b/cmd/driver/main.go @@ -13,6 +13,7 @@ import ( "github.com/iden3/driver-did-iden3/pkg/services" "github.com/iden3/driver-did-iden3/pkg/services/blockchain/eth" "github.com/iden3/driver-did-iden3/pkg/services/ens" + "github.com/iden3/driver-did-iden3/pkg/services/signers" ) func main() { @@ -34,7 +35,7 @@ func main() { } mux := app.Handlers{DidDocumentHandler: &app.DidDocumentHandler{ - DidDocumentService: services.NewDidDocumentServices(initResolvers(), r), + DidDocumentService: services.NewDidDocumentServices(initResolvers(), r, services.WithSigners(initEIP712Signers())), }, } @@ -63,8 +64,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, - eth.WithSigner(networkSettings.WalletKey)) + resolver, err := eth.NewResolver(networkSettings.NetworkURL, networkSettings.ContractAddress) if err != nil { log.Fatalf("failed configure resolver for network '%s': %v", prefix, err) } @@ -74,3 +74,27 @@ func initResolvers() *services.ResolverRegistry { return resolvers } + +func initEIP712Signers() *services.EIP712SignerRegistry { + var path string + if len(os.Args) > 3 { + path = os.Args[2] + } + rs, err := configs.ParseSignersSettings(path) + if err != nil { + log.Fatal("can't read signers settings:", err) + } + chainSigners := services.NewChainEIP712Signers() + for chainName, chainSettings := range rs { + for networkName, networkSettings := range chainSettings { + prefix := fmt.Sprintf("%s:%s", chainName, networkName) + signer, err := signers.NewEIP712Signer(networkSettings.WalletKey) + if err != nil { + log.Fatalf("failed configure signer for network '%s': %v", prefix, err) + } + chainSigners.Add(prefix, signer) + } + } + + return chainSigners +} diff --git a/pkg/app/configs/driver.go b/pkg/app/configs/driver.go index e3be040..9e0f093 100644 --- a/pkg/app/configs/driver.go +++ b/pkg/app/configs/driver.go @@ -11,12 +11,16 @@ import ( ) const defaultPathToResolverSettings = "./resolvers.settings.yaml" +const defaultPathToSignersSettings = "./signers.settings.yaml" // ResolverSettings represent settings for resolver. type ResolverSettings map[string]map[string]struct { ContractAddress string `yaml:"contractAddress"` NetworkURL string `yaml:"networkURL"` - WalletKey string `yaml:"walletKey"` +} + +type SignersSettings map[string]map[string]struct { + WalletKey string `yaml:"walletKey"` } // Config structure represent yaml config for did driver. @@ -61,3 +65,26 @@ func ParseResolversSettings(path string) (ResolverSettings, error) { return settings, nil } + +// ParseSignersSettings parse yaml file with signers settings. +func ParseSignersSettings(path string) (SignersSettings, error) { + if path == "" { + path = defaultPathToSignersSettings + } + f, err := os.Open(filepath.Clean(path)) + if err != nil { + return nil, err + } + defer func() { + if err := f.Close(); err != nil { + log.Println("failed to close setting file:", err) + } + }() + + settings := SignersSettings{} + if err := yaml.NewDecoder(f).Decode(&settings); err != nil { + return nil, errors.Errorf("invalid yaml file: %v", settings) + } + + return settings, nil +} diff --git a/pkg/services/blockchain/eth/resolver.go b/pkg/services/blockchain/eth/resolver.go index 7687d9e..0622e43 100644 --- a/pkg/services/blockchain/eth/resolver.go +++ b/pkg/services/blockchain/eth/resolver.go @@ -2,22 +2,13 @@ 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" @@ -39,70 +30,18 @@ type Resolver struct { contractAddress string chainID int - walletKey string -} - -type AuthData struct { - TypedData apitypes.TypedData - Signature string - Address string } type ResolverOption func(*Resolver) -const ( - secp256k1VValue = 27 -) - var ( gistNotFoundException = "execution reverted: Root does not exist" identityNotFoundException = "execution reverted: Identity does not exist" stateNotFoundException = "execution reverted: State does not exist" ) -var IdentityStateAPITypes = apitypes.Types{ - "IdentityState": []apitypes.Type{ - {Name: "timestamp", Type: "uint256"}, - {Name: "id", Type: "uint256"}, - {Name: "state", 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: "timestamp", Type: "uint256"}, - {Name: "idType", Type: "bytes2"}, - {Name: "root", 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 - -func WithSigner(walletKey string) ResolverOption { - return func(r *Resolver) { - if walletKey != "" { - r.walletKey = walletKey - return - } - } -} - // NewResolver create new ethereum resolver. -func NewResolver(url string, address string, opts ...ResolverOption) (*Resolver, error) { +func NewResolver(url, address string) (*Resolver, error) { c, err := ethclient.Dial(url) if err != nil { return nil, err @@ -116,9 +55,6 @@ func NewResolver(url string, address string, opts ...ResolverOption) (*Resolver, state: sc, contractAddress: address, } - for _, opt := range opts { - opt(resolver) - } chainID, err := c.NetworkID(context.Background()) if err != nil { @@ -132,27 +68,6 @@ func (r *Resolver) BlockchainID() string { return fmt.Sprintf("%d:%s", r.chainID, r.contractAddress) } -func (r *Resolver) GetWalletAddress() (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, @@ -259,208 +174,9 @@ func (r *Resolver) Resolve( } } - signature := "" - if opts.Signature != "" { - if r.walletKey == "" { - return services.IdentityState{}, - errors.New("no wallet key found for generating signature") - } - primaryType := services.IdentityStateType - if opts.GistRoot != 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) { - walletAddress, err := r.GetWalletAddress() - if err != nil { - return false, err - } - - typedData, err := r.TypedData(primaryType, did, identityState, walletAddress) - if err != nil { - return false, err - } - - authData := AuthData{TypedData: typedData, Signature: identityState.Signature, Address: walletAddress} - 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) { - if primaryType == services.IdentityStateType && identityState.StateInfo == nil { - return apitypes.TypedData{}, - errors.New("identity state info is required for primary type 'IdentityState'") - } - if primaryType == services.GlobalStateType && identityState.GistInfo == nil { - return apitypes.TypedData{}, - errors.New("gist info is required for primary type 'GlobalState'") - } - id, err := core.IDFromDID(did) - if err != nil { - return apitypes.TypedData{}, - fmt.Errorf("invalid did format for did '%s': %v", did, err) - } - ID := id.BigInt().String() - idType := fmt.Sprintf("0x%X", id.Type()) - - apiTypes := apitypes.Types{} - message := apitypes.TypedDataMessage{} - primaryTypeString := "" - timestamp := TimeStamp() - - switch primaryType { - case services.IdentityStateType: - state := "0" - replacedAtTimestamp := "0" - state = identityState.StateInfo.State.String() - replacedAtTimestamp = identityState.StateInfo.ReplacedAtTimestamp.String() - primaryTypeString = "IdentityState" - apiTypes = IdentityStateAPITypes - message = apitypes.TypedDataMessage{ - "timestamp": timestamp, - "id": ID, - "state": state, - "replacedAtTimestamp": replacedAtTimestamp, - } - case services.GlobalStateType: - root := "0" - replacedAtTimestamp := "0" - root = identityState.GistInfo.Root.String() - replacedAtTimestamp = identityState.GistInfo.ReplacedAtTimestamp.String() - primaryTypeString = "GlobalState" - apiTypes = GlobalStateAPITypes - message = apitypes.TypedDataMessage{ - "timestamp": timestamp, - "idType": idType, - "root": root, - "replacedAtTimestamp": replacedAtTimestamp, - } - } - - typedData := apitypes.TypedData{ - Types: apiTypes, - PrimaryType: primaryTypeString, - Domain: apitypes.TypedDataDomain{ - Name: "StateInfo", - Version: "1", - ChainId: math.NewHexOrDecimal256(int64(0)), - VerifyingContract: common.Address{}.String(), - }, - 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 - } - - walletAddress, err := r.GetWalletAddress() - if err != nil { - return "", err - } - - typedData, err := r.TypedData(primaryType, did, identityState, walletAddress) - 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] < secp256k1VValue { // Invalid Ethereum signature (V is not 27 or 28) - signature[64] += secp256k1VValue // Transform yellow paper V from 0/1 to 27/28 - } - - 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, diff --git a/pkg/services/blockchain/eth/resolver_test.go b/pkg/services/blockchain/eth/resolver_test.go index 7b7e56b..d497031 100644 --- a/pkg/services/blockchain/eth/resolver_test.go +++ b/pkg/services/blockchain/eth/resolver_test.go @@ -6,18 +6,14 @@ 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/document" "github.com/iden3/driver-did-iden3/pkg/services" cm "github.com/iden3/driver-did-iden3/pkg/services/blockchain/eth/contract/mock" core "github.com/iden3/go-iden3-core/v2" "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:amoy:2qY71pSkdCsRetTHbUA4YqG7Hx63Ej2PeiJMzAdJ2V") @@ -205,145 +201,3 @@ func TestNotFoundErr(t *testing.T) { }) } } - -func TestResolveSignature_Success(t *testing.T) { - 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: string(document.EthereumEip712SignatureProof2021Type), - }, - 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: "0x388e838580e95a771a10806ea1514ab441e9f598ccca01899dea8541411a631c1d67525c652b1662c51547ea0aad445bc4e9d0fc3d41802221e66b0f534526841b", - }, - }, - { - name: "resolve identity state by state", - opts: &services.ResolverOpts{ - State: big.NewInt(1), - Signature: string(document.EthereumEip712SignatureProof2021Type), - }, - 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: "0x3bf7344312b0ef482974de45c722fbd431316b0bc42bd1050b5cb7bbe53034c51aa885d72c6cd958bdc3b46fc247f38b67c03767d98ba815ae3d8c33aac7398c1c", - }, - }, - { - name: "resolve latest state", - opts: &services.ResolverOpts{ - Signature: string(document.EthereumEip712SignatureProof2021Type), - }, - 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: "0x3bf7344312b0ef482974de45c722fbd431316b0bc42bd1050b5cb7bbe53034c51aa885d72c6cd958bdc3b46fc247f38b67c03767d98ba815ae3d8c33aac7398c1c", - }, - }, - } - - 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.opts.GistRoot != 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 6ab7bbb..6410e66 100644 --- a/pkg/services/did.go +++ b/pkg/services/did.go @@ -5,9 +5,13 @@ import ( "fmt" "math/big" "net" + "strconv" "strings" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/iden3/driver-did-iden3/pkg/document" "github.com/iden3/driver-did-iden3/pkg/services/ens" core "github.com/iden3/go-iden3-core/v2" @@ -23,6 +27,7 @@ const ( type DidDocumentServices struct { resolvers *ResolverRegistry ens *ens.Registry + signers *EIP712SignerRegistry } type ResolverOpts struct { @@ -31,8 +36,21 @@ type ResolverOpts struct { Signature string } -func NewDidDocumentServices(resolvers *ResolverRegistry, registry *ens.Registry) *DidDocumentServices { - return &DidDocumentServices{resolvers, registry} +type DidDocumentOption func(*DidDocumentServices) + +func WithSigners(signers *EIP712SignerRegistry) DidDocumentOption { + return func(d *DidDocumentServices) { + d.signers = signers + } +} + +func NewDidDocumentServices(resolvers *ResolverRegistry, registry *ens.Registry, opts ...DidDocumentOption) *DidDocumentServices { + didDocumentService := &DidDocumentServices{resolvers, registry, nil} + + for _, opt := range opts { + opt(didDocumentService) + } + return didDocumentService } // GetDidDocument return did document by identifier. @@ -134,25 +152,36 @@ func (d *DidDocumentServices) GetDidDocument(ctx context.Context, did string, op }, ) - walletAddress, err := resolver.GetWalletAddress() - - if err == nil && opts.Signature != "" { - primaryType := IdentityStateType - if opts.GistRoot != nil { - primaryType = GlobalStateType + if opts.Signature != "" { + if d.signers == nil { + return nil, errors.New("signers not initialized") } - eip712TypedData, err := resolver.TypedData(primaryType, *userDID, identityState, walletAddress) + signer, err := d.signers.GetEIP712SignerByNetwork(string(b), string(n)) if err != nil { - return nil, fmt.Errorf("invalid typed data: %v", err) + return nil, fmt.Errorf("invalid signer: %v", err) + } + errResolution, err = expectedError(err) + if err != nil { + return errResolution, 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(), + var eip712TypedData *apitypes.TypedData + if opts.GistRoot != nil { + typedData, err := getTypedData(GlobalStateType, *userDID, identityState) + if err != nil { + return nil, fmt.Errorf("invalid typed data for global state: %v", err) + } + eip712TypedData = &typedData + } else { + typedData, err := getTypedData(IdentityStateType, *userDID, identityState) + if err != nil { + return nil, fmt.Errorf("invalid typed data for identity state: %v", err) + } + eip712TypedData = &typedData + } + eip712Proof, err := signer.Sign(*eip712TypedData) + if err != nil { + return nil, fmt.Errorf("invalid eip712 typed data: %v", err) } didResolution.DidResolutionMetadata.Context = document.DidResolutionMetadataSigContext() @@ -161,6 +190,95 @@ func (d *DidDocumentServices) GetDidDocument(ctx context.Context, did string, op return didResolution, nil } +func getTypedData(typedDataType TypedDataType, did w3c.DID, identityState IdentityState) (apitypes.TypedData, error) { + id, err := core.IDFromDID(did) + if err != nil { + return apitypes.TypedData{}, + fmt.Errorf("invalid did format for did '%s': %v", did, err) + } + + timestamp := timeStamp() + + var apiTypes apitypes.Types + var message apitypes.TypedDataMessage + var primaryType string + + switch typedDataType { + case IdentityStateType: + primaryType = "IdentityState" + apiTypes = apitypes.Types{ + "IdentityState": []apitypes.Type{ + {Name: "timestamp", Type: "uint256"}, + {Name: "id", Type: "uint256"}, + {Name: "state", 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"}, + }, + } + ID := id.BigInt().String() + state := identityState.StateInfo.State.String() + replacedAtTimestamp := identityState.StateInfo.ReplacedAtTimestamp.String() + message = apitypes.TypedDataMessage{ + "timestamp": timestamp, + "id": ID, + "state": state, + "replacedAtTimestamp": replacedAtTimestamp, + } + + case GlobalStateType: + primaryType = "GlobalState" + apiTypes = apitypes.Types{ + "GlobalState": []apitypes.Type{ + {Name: "timestamp", Type: "uint256"}, + {Name: "idType", Type: "bytes2"}, + {Name: "root", 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"}, + }, + } + idType := fmt.Sprintf("0x%X", id.Type()) + root := identityState.GistInfo.Root.String() + replacedAtTimestamp := identityState.GistInfo.ReplacedAtTimestamp.String() + message = apitypes.TypedDataMessage{ + "timestamp": timestamp, + "idType": idType, + "root": root, + "replacedAtTimestamp": replacedAtTimestamp, + } + default: + return apitypes.TypedData{}, fmt.Errorf("typedDataType %d not defined", typedDataType) + } + + typedData := apitypes.TypedData{ + Types: apiTypes, + PrimaryType: primaryType, + Domain: apitypes.TypedDataDomain{ + Name: "StateInfo", + Version: "1", + ChainId: math.NewHexOrDecimal256(int64(0)), + VerifyingContract: common.Address{}.String(), + }, + Message: message, + } + + return typedData, nil +} + +func timeStamp() string { + timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10) + return timestamp +} + // ResolveDNSDomain return did document by domain via DNS. func (d *DidDocumentServices) ResolveDNSDomain(ctx context.Context, domain string) (*document.DidResolution, error) { domain = fmt.Sprintf("_did.%s", domain) diff --git a/pkg/services/registry.go b/pkg/services/registry.go index f3cd834..c4c688d 100644 --- a/pkg/services/registry.go +++ b/pkg/services/registry.go @@ -5,7 +5,6 @@ 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" @@ -19,13 +18,6 @@ var ( ErrNotFound = errors.New("not found") ) -type PrimaryType int32 - -const ( - IdentityStateType PrimaryType = 0 - GlobalStateType PrimaryType = 1 -) - type IdentityState struct { StateInfo *StateInfo GistInfo *GistInfo @@ -104,8 +96,6 @@ type Resolver interface { Resolve(ctx context.Context, did w3c.DID, opts *ResolverOpts) (IdentityState, error) ResolveGist(ctx context.Context, opts *ResolverOpts) (*GistInfo, error) BlockchainID() string - GetWalletAddress() (string, error) - TypedData(primaryType PrimaryType, did w3c.DID, identityState IdentityState, walletAddress string) (apitypes.TypedData, error) } type ResolverRegistry map[string]Resolver diff --git a/pkg/services/signers.go b/pkg/services/signers.go new file mode 100644 index 0000000..813f794 --- /dev/null +++ b/pkg/services/signers.go @@ -0,0 +1,46 @@ +package services + +import ( + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/iden3/driver-did-iden3/pkg/document" +) + +type TypedDataType int32 + +const ( + IdentityStateType TypedDataType = 0 + GlobalStateType TypedDataType = 1 +) + +type EIP712Signer interface { + Sign(typedData apitypes.TypedData) (*document.EthereumEip712SignatureProof2021, error) +} + +type EIP712SignerRegistry map[string]EIP712Signer + +func NewChainEIP712Signers() *EIP712SignerRegistry { + return &EIP712SignerRegistry{} +} + +func (ch *EIP712SignerRegistry) Add(prefix string, signer EIP712Signer) { + (*ch)[prefix] = signer +} + +func (ch *EIP712SignerRegistry) Append(prefix string, signer EIP712Signer) error { + _, ok := (*ch)[prefix] + if ok { + return ErrResolverAlreadyExists + } + (*ch)[prefix] = signer + return nil +} + +func (ch *EIP712SignerRegistry) GetEIP712SignerByNetwork(chain, networkID string) (EIP712Signer, error) { + p := resolverPrefix(chain, networkID) + signer, ok := (*ch)[p] + if !ok { + return nil, ErrNetworkIsNotSupported + } + + return signer, nil +} diff --git a/pkg/services/signers/EIP712Signer.go b/pkg/services/signers/EIP712Signer.go new file mode 100644 index 0000000..4f0b179 --- /dev/null +++ b/pkg/services/signers/EIP712Signer.go @@ -0,0 +1,93 @@ +package signers + +import ( + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/iden3/driver-did-iden3/pkg/document" +) + +const ( + secp256k1VValue = 27 +) + +type EIP712Signer struct { + walletKey string +} + +func NewEIP712Signer(walletKey string) (*EIP712Signer, error) { + globalStateSigner := &EIP712Signer{ + walletKey: walletKey, + } + + return globalStateSigner, nil +} + +func (s *EIP712Signer) getWalletAddress() (string, error) { + if s.walletKey == "" { + return "", errors.New("wallet key is not set") + } + + privateKey, err := crypto.HexToECDSA(s.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 (s *EIP712Signer) Sign(typedData apitypes.TypedData) (*document.EthereumEip712SignatureProof2021, error) { + privateKey, err := crypto.HexToECDSA(s.walletKey) + if err != nil { + return nil, err + } + walletAddress, err := s.getWalletAddress() + if err != nil { + return nil, err + } + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return nil, errors.New("error hashing EIP712Domain for signing") + } + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return nil, 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 nil, err + } + + if signature[64] < secp256k1VValue { // Invalid Ethereum signature (V is not 27 or 28) + signature[64] += secp256k1VValue // Transform yellow paper V from 0/1 to 27/28 + } + + messageSignature := "0x" + hex.EncodeToString(signature) + + eip712Proof := &document.EthereumEip712SignatureProof2021{ + Type: document.EthereumEip712SignatureProof2021Type, + ProofPursopose: "assertionMethod", + ProofValue: messageSignature, + VerificationMethod: fmt.Sprintf("did:pkh:eip155:0:%s#blockchainAccountId", walletAddress), + Eip712: typedData, + Created: time.Now(), + } + + return eip712Proof, nil +}