From 6ad8d82655c11fc778e97a0eec4507b194f1d450 Mon Sep 17 00:00:00 2001 From: Zac Lew Date: Mon, 22 Apr 2024 09:51:53 +0200 Subject: [PATCH] Secret Keeper Application (#745) - implement secret keeper demo - readme - create unit test mock rollback attack --------- Signed-off-by: chenchanglew Signed-off-by: chenchanglew --- samples/chaincode/secret-keeper-go/.gitignore | 7 + samples/chaincode/secret-keeper-go/Makefile | 9 + samples/chaincode/secret-keeper-go/README.md | 54 ++++ .../chaincode/secret-keeper.go | 205 +++++++++++++ .../chaincode/secret-keeper_test.go | 272 ++++++++++++++++++ samples/chaincode/secret-keeper-go/main.go | 41 +++ 6 files changed, 588 insertions(+) create mode 100644 samples/chaincode/secret-keeper-go/.gitignore create mode 100644 samples/chaincode/secret-keeper-go/Makefile create mode 100644 samples/chaincode/secret-keeper-go/README.md create mode 100644 samples/chaincode/secret-keeper-go/chaincode/secret-keeper.go create mode 100644 samples/chaincode/secret-keeper-go/chaincode/secret-keeper_test.go create mode 100644 samples/chaincode/secret-keeper-go/main.go diff --git a/samples/chaincode/secret-keeper-go/.gitignore b/samples/chaincode/secret-keeper-go/.gitignore new file mode 100644 index 000000000..e8df032d2 --- /dev/null +++ b/samples/chaincode/secret-keeper-go/.gitignore @@ -0,0 +1,7 @@ +ecc +ecc-bundle +enclave.json +private.pem +public.pem +mrenclave +details.env diff --git a/samples/chaincode/secret-keeper-go/Makefile b/samples/chaincode/secret-keeper-go/Makefile new file mode 100644 index 000000000..8f80af00d --- /dev/null +++ b/samples/chaincode/secret-keeper-go/Makefile @@ -0,0 +1,9 @@ +# Copyright 2019 Intel Corporation +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +TOP = ../../.. +include $(TOP)/ecc_go/build.mk + +CC_NAME ?= fpc-secret-keeper-go diff --git a/samples/chaincode/secret-keeper-go/README.md b/samples/chaincode/secret-keeper-go/README.md new file mode 100644 index 000000000..c309fbbde --- /dev/null +++ b/samples/chaincode/secret-keeper-go/README.md @@ -0,0 +1,54 @@ +# Secret Keeper + +Secret Keeper is a demo application designed to securely store sensitive information, acting as a digital vault. It's ideal for users who need to manage access to shared secrets within a team or organization. + +## Functions + +Secret Keeper provides the following functionalities: + +- **InitSecretKeeper**: Initializes the application with default authorization and secret values. Intended for one-time use at application setup. Note: While potential misuse is considered low-risk, it's recommended to secure access to this function. + +- **RevealSecret**: Allows authorized users to view the currently stored secret. + +- **LockSecret**: Enables authorized users to update the secret value. This action replaces the existing secret. + +- **AddUser**: Permits authorized users to add a new user to the authorization list, granting them access to all functions. + +- **RemoveUser**: Allows authorized users to remove an existing user from the authorization list, revoking their access. + +## Example Usage + +To demonstrate Secret Keeper's capabilities, you can deploy the chaincode to [the-simple-testing-network](https://github.com/hyperledger/fabric-private-chaincode/tree/main/samples/deployment/fabric-smart-client/the-simple-testing-network) and then invoke it with the [simple-cli-go](https://github.com/hyperledger/fabric-private-chaincode/tree/main/samples/application/simple-cli-go). + +1. Initialize Secret Keeper: +``` +./fpcclient invoke initSecretKeeper +``` +2. Reveal the secret as Alice: +``` +./fpcclient query revealSecret Alice +``` +3. Change the secret as Bob: +``` +./fpcclient invoke lockSecret Bob NewSecret +``` +4. Attempt to reveal the secret as Alice (now updated): +``` +./fpcclient query revealSecret Alice +``` +5. Remove Bob's access as Alice: +``` +./fpcclient invoke removeUser Alice Bob +``` +6. Attempt to reveal the secret as Bob (should fail): +``` +./fpcclient query revealSecret Bob // (will failed) +``` +7. Re-add Bob to the authorization list as Alice: +``` +./fpcclient invoke addUser Alice Bob +``` +8. Bob can now reveal the secret successfully: +``` +./fpcclient query revealSecret Bob // (will success) +``` diff --git a/samples/chaincode/secret-keeper-go/chaincode/secret-keeper.go b/samples/chaincode/secret-keeper-go/chaincode/secret-keeper.go new file mode 100644 index 000000000..b4c38f5b6 --- /dev/null +++ b/samples/chaincode/secret-keeper-go/chaincode/secret-keeper.go @@ -0,0 +1,205 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package chaincode + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-contract-api-go/contractapi" +) + +const OK = "OK" +const AUTH_LIST_KEY = "AUTH_LIST_KEY" +const SECRET_KEY = "SECRET_KEY" + +type SecretKeeper struct { + contractapi.Contract +} + +type AuthSet struct { + Pubkey map[string]struct{} +} + +type Secret struct { + Value string `json:"Value"` +} + +func (t *SecretKeeper) InitSecretKeeper(ctx contractapi.TransactionContextInterface) error { + // init authSet + pubkeyset := make(map[string]struct{}) + pubkeyset["Alice"] = struct{}{} + pubkeyset["Bob"] = struct{}{} + authSet := AuthSet{ + Pubkey: pubkeyset, + } + + authSetJson, err := json.Marshal(authSet) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(AUTH_LIST_KEY, authSetJson) + if err != nil { + return fmt.Errorf("failed to put %s to world state. %v", AUTH_LIST_KEY, err) + } + + // init secret + secret := Secret{ + Value: "DefaultSecret", + } + + secretJson, err := json.Marshal(secret) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(SECRET_KEY, secretJson) + if err != nil { + return fmt.Errorf("failed to put %s to world state. %v", SECRET_KEY, err) + } + + return nil +} + +func (t *SecretKeeper) AddUser(ctx contractapi.TransactionContextInterface, sig string, pubkey string) error { + // check if the user allow to update authSet + valid, err := VerifySig(ctx, sig) + if err != nil { + return err + } + if !valid { + return fmt.Errorf("user are not allowed to perform this action") + } + + // update the value + authSet, _ := GetAuthList(ctx) + authSet.Pubkey[pubkey] = struct{}{} + + authSetJson, err := json.Marshal(authSet) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(AUTH_LIST_KEY, authSetJson) + if err != nil { + return fmt.Errorf("failed to put %s to world state. %v", AUTH_LIST_KEY, err) + } + + return nil +} + +func (t *SecretKeeper) RemoveUser(ctx contractapi.TransactionContextInterface, sig string, pubkey string) error { + // check if the user allow to update authSet + valid, err := VerifySig(ctx, sig) + if err != nil { + return err + } + if !valid { + return fmt.Errorf("user are not allowed to perform this action") + } + + // update the value + authSet, _ := GetAuthList(ctx) + delete(authSet.Pubkey, pubkey) + + authSetJson, err := json.Marshal(authSet) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(AUTH_LIST_KEY, authSetJson) + if err != nil { + return fmt.Errorf("failed to put %s to world state. %v", AUTH_LIST_KEY, err) + } + + return nil +} + +func (t *SecretKeeper) LockSecret(ctx contractapi.TransactionContextInterface, sig string, value string) error { + // check if the user allow to update secret + valid, err := VerifySig(ctx, sig) + if err != nil { + return err + } + if !valid { + return fmt.Errorf("user are not allowed to perform this action") + } + + // update the value + newSecret := Secret{ + Value: value, + } + + newSecretJson, err := json.Marshal(newSecret) + if err != nil { + return err + } + + err = ctx.GetStub().PutState(SECRET_KEY, newSecretJson) + if err != nil { + return fmt.Errorf("failed to put %s to world state. %v", SECRET_KEY, err) + } + + return nil +} + +func (t *SecretKeeper) RevealSecret(ctx contractapi.TransactionContextInterface, sig string) (*Secret, error) { + // check if the user allow to view the secret. + valid, err := VerifySig(ctx, sig) + if err != nil { + return nil, err + } + if !valid { + return nil, fmt.Errorf("user are not allowed to perform this action") + } + + // reveal secret + secretJson, err := ctx.GetStub().GetState(SECRET_KEY) + if err != nil { + return nil, fmt.Errorf("failed to read from world state: %v", err) + } + if secretJson == nil { + return nil, fmt.Errorf("the asset %s does not exist", SECRET_KEY) + } + var secret Secret + err = json.Unmarshal(secretJson, &secret) + if err != nil { + return nil, err + } + return &secret, nil +} + +func GetAuthList(ctx contractapi.TransactionContextInterface) (*AuthSet, error) { + authSetJson, err := ctx.GetStub().GetState(AUTH_LIST_KEY) + if err != nil { + return nil, fmt.Errorf("failed to read from world state: %v", err) + } + if authSetJson == nil { + return nil, fmt.Errorf("the asset %s does not exist", AUTH_LIST_KEY) + } + + var authSet AuthSet + err = json.Unmarshal(authSetJson, &authSet) + if err != nil { + return nil, err + } + return &authSet, nil +} + +func VerifySig(ctx contractapi.TransactionContextInterface, sig string) (bool, error) { + authSet, err := GetAuthList(ctx) + if err != nil { + return false, err + } + + if _, exist := authSet.Pubkey[sig]; exist { + return true, nil + } + + return false, nil +} diff --git a/samples/chaincode/secret-keeper-go/chaincode/secret-keeper_test.go b/samples/chaincode/secret-keeper-go/chaincode/secret-keeper_test.go new file mode 100644 index 000000000..8f49e2d10 --- /dev/null +++ b/samples/chaincode/secret-keeper-go/chaincode/secret-keeper_test.go @@ -0,0 +1,272 @@ +package chaincode + +import ( + "encoding/json" + "testing" + + "github.com/hyperledger/fabric-private-chaincode/ercc/registry/fakes" + "github.com/stretchr/testify/require" +) + +func TestWrongSignature(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + + chaincodeStub.GetStateReturns(authSetByte, nil) + + falseSig := "falseSignature" + fakeSecret := "fakeSecret" + err = secretKeeper.AddUser(transactionContext, falseSig, falseSig) + require.EqualError(t, err, "User are not allowed to perform this action.") + + err = secretKeeper.RemoveUser(transactionContext, falseSig, falseSig) + require.EqualError(t, err, "User are not allowed to perform this action.") + + err = secretKeeper.LockSecret(transactionContext, falseSig, fakeSecret) + require.EqualError(t, err, "User are not allowed to perform this action.") + + secret, err := secretKeeper.RevealSecret(transactionContext, falseSig) + require.EqualError(t, err, "User are not allowed to perform this action.") + require.Nil(t, secret) +} + +func TestAddUserFlow(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + _, _ = chaincodeStub.PutStateArgsForCall(1) // get default secret + + chaincodeStub.GetStateReturns(authSetByte, nil) + + aliceSig := "Alice" + evePubKey := "Eve" + + // check if authlist not contains eve + var authSet AuthSet + err = json.Unmarshal(authSetByte, &authSet) + require.NoError(t, err) + _, exist := authSet.Pubkey[evePubKey] + require.False(t, exist) + + err = secretKeeper.AddUser(transactionContext, aliceSig, evePubKey) + require.NoError(t, err) + + // check if authlist contains eve. + _, authSetByte2 := chaincodeStub.PutStateArgsForCall(2) + var authSet2 AuthSet + err = json.Unmarshal(authSetByte2, &authSet2) + require.NoError(t, err) + _, exist = authSet2.Pubkey[evePubKey] + require.True(t, exist) +} + +func TestRemoveUser(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + _, _ = chaincodeStub.PutStateArgsForCall(1) // get default secret + + chaincodeStub.GetStateReturns(authSetByte, nil) + + aliceSig := "Alice" + bobPubKey := "Bob" + + // check if authlist contains bob. + var authSet AuthSet + err = json.Unmarshal(authSetByte, &authSet) + require.NoError(t, err) + _, exist := authSet.Pubkey[bobPubKey] + require.True(t, exist) + + err = secretKeeper.RemoveUser(transactionContext, aliceSig, bobPubKey) + require.NoError(t, err) + + // check if authlist doesn't contain bob anymore. + _, authSetByte2 := chaincodeStub.PutStateArgsForCall(2) + var authSet2 AuthSet + err = json.Unmarshal(authSetByte2, &authSet2) + require.NoError(t, err) + _, exist = authSet2.Pubkey[bobPubKey] + require.False(t, exist) +} + +func TestLockSecret(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + _, _ = chaincodeStub.PutStateArgsForCall(1) // get default secret + + chaincodeStub.GetStateReturns(authSetByte, nil) + + aliceSig := "Alice" + newSecret := "newSecret" + + err = secretKeeper.LockSecret(transactionContext, aliceSig, newSecret) + require.NoError(t, err) + + // check secret key value. + _, secretByte := chaincodeStub.PutStateArgsForCall(2) + var secret Secret + err = json.Unmarshal(secretByte, &secret) + require.NoError(t, err) + require.EqualValues(t, secret.Value, newSecret) +} + +func TestRevealSecret(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + _, defaultSecretByte := chaincodeStub.PutStateArgsForCall(1) + + aliceSig := "Alice" + var defaultSecret Secret + err = json.Unmarshal(defaultSecretByte, &defaultSecret) + require.NoError(t, err) + + // check the return value equal with the secret in test. + chaincodeStub.GetStateReturnsOnCall(0, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(1, defaultSecretByte, nil) + secret, err := secretKeeper.RevealSecret(transactionContext, aliceSig) + require.NoError(t, err) + require.EqualValues(t, secret.Value, defaultSecret.Value) +} + +func TestNormalBehavior(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + _, _ = chaincodeStub.PutStateArgsForCall(1) + + aliceSig := "Alice" + bobSig := "Bob" + eveSig := "Eve" + newSecret := "NewSecret" + newSecret2 := "SecretWithoutAlice" + + chaincodeStub.GetStateReturnsOnCall(0, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(1, authSetByte, nil) + err = secretKeeper.AddUser(transactionContext, aliceSig, eveSig) + require.NoError(t, err) + _, authSetByte = chaincodeStub.PutStateArgsForCall(2) + + chaincodeStub.GetStateReturnsOnCall(2, authSetByte, nil) + err = secretKeeper.LockSecret(transactionContext, eveSig, newSecret) + require.NoError(t, err) + _, secretByte := chaincodeStub.PutStateArgsForCall(3) + + chaincodeStub.GetStateReturnsOnCall(3, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(4, secretByte, nil) + secret, err := secretKeeper.RevealSecret(transactionContext, aliceSig) + require.NoError(t, err) + require.EqualValues(t, secret.Value, newSecret) + + chaincodeStub.GetStateReturnsOnCall(5, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(6, authSetByte, nil) + err = secretKeeper.RemoveUser(transactionContext, eveSig, aliceSig) + require.NoError(t, err) + _, authSetByte = chaincodeStub.PutStateArgsForCall(4) + + chaincodeStub.GetStateReturnsOnCall(7, authSetByte, nil) + err = secretKeeper.LockSecret(transactionContext, bobSig, newSecret2) + require.NoError(t, err) + _, secretByte = chaincodeStub.PutStateArgsForCall(5) + + chaincodeStub.GetStateReturnsOnCall(8, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(9, secretByte, nil) + secret, err = secretKeeper.RevealSecret(transactionContext, aliceSig) + require.EqualError(t, err, "User are not allowed to perform this action.") + require.Nil(t, secret) +} + +func TestRollbackAttack(t *testing.T) { + chaincodeStub := &fakes.ChaincodeStub{} + transactionContext := &fakes.TransactionContext{} + transactionContext.GetStubReturns(chaincodeStub) + + secretKeeper := SecretKeeper{} + err := secretKeeper.InitSecretKeeper(transactionContext) + require.NoError(t, err) + + _, authSetByte := chaincodeStub.PutStateArgsForCall(0) + _, _ = chaincodeStub.PutStateArgsForCall(1) + oldauthSetByte := authSetByte + + aliceSig := "Alice" + bobSig := "Bob" + eveSig := "Eve" + newSecret := "NewSecret" + newSecret2 := "SecretWithoutAlice" + + chaincodeStub.GetStateReturnsOnCall(0, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(1, authSetByte, nil) + err = secretKeeper.AddUser(transactionContext, aliceSig, eveSig) + require.NoError(t, err) + _, authSetByte = chaincodeStub.PutStateArgsForCall(2) + + chaincodeStub.GetStateReturnsOnCall(2, authSetByte, nil) + err = secretKeeper.LockSecret(transactionContext, eveSig, newSecret) + require.NoError(t, err) + _, secretByte := chaincodeStub.PutStateArgsForCall(3) + + chaincodeStub.GetStateReturnsOnCall(3, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(4, secretByte, nil) + secret, err := secretKeeper.RevealSecret(transactionContext, aliceSig) + require.NoError(t, err) + require.EqualValues(t, secret.Value, newSecret) + + chaincodeStub.GetStateReturnsOnCall(5, authSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(6, authSetByte, nil) + err = secretKeeper.RemoveUser(transactionContext, eveSig, aliceSig) + require.NoError(t, err) + + _, authSetByte = chaincodeStub.PutStateArgsForCall(4) + + chaincodeStub.GetStateReturnsOnCall(7, authSetByte, nil) + err = secretKeeper.LockSecret(transactionContext, bobSig, newSecret2) + require.NoError(t, err) + _, secretByte = chaincodeStub.PutStateArgsForCall(5) + + // Simulate rollback attack here + chaincodeStub.GetStateReturnsOnCall(8, oldauthSetByte, nil) + chaincodeStub.GetStateReturnsOnCall(9, secretByte, nil) + secret, err = secretKeeper.RevealSecret(transactionContext, aliceSig) + require.NoError(t, err) + require.EqualValues(t, secret.Value, newSecret2) +} diff --git a/samples/chaincode/secret-keeper-go/main.go b/samples/chaincode/secret-keeper-go/main.go new file mode 100644 index 000000000..a8f707f1b --- /dev/null +++ b/samples/chaincode/secret-keeper-go/main.go @@ -0,0 +1,41 @@ +/* +Copyright IBM Corp. All Rights Reserved. +Copyright 2020 Intel Corporation + +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "os" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-contract-api-go/contractapi" + fpc "github.com/hyperledger/fabric-private-chaincode/ecc_go/chaincode" + "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/secret-keeper-go/chaincode" +) + +func main() { + + ccid := os.Getenv("CHAINCODE_PKG_ID") + addr := os.Getenv("CHAINCODE_SERVER_ADDRESS") + + // create chaincode + secretChaincode, _ := contractapi.NewChaincode(&chaincode.SecretKeeper{}) + chaincode := fpc.NewPrivateChaincode(secretChaincode) + + // start chaincode as a service + server := &shim.ChaincodeServer{ + CCID: ccid, + Address: addr, + CC: chaincode, + TLSProps: shim.TLSProperties{ + Disabled: true, // just for testing good enough + }, + } + + if err := server.Start(); err != nil { + panic(err) + } +}