Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cwt proof #40

Merged
merged 17 commits into from
Jan 30, 2024
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ dist
build
umd

# Auto-generated mock files
*_mocks_test.go
gomocks_test.go
29 changes: 25 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,47 @@ DOCKER_CMD ?= docker
GOBIN_PATH=$(abspath .)/build/bin
MOCKGEN=$(GOBIN_PATH)/mockgen
GOMOCKS=pkg/internal/gomocks
MOCK_VERSION ?=v1.7.0-rc.1

OS := $(shell uname)
ifeq ($(OS),$(filter $(OS),Darwin Linux))
PATH:=$(PATH):$(GOBIN_PATH)
else
PATH:=$(PATH);$(subst /,\\,$(GOBIN_PATH))
endif

.PHONY: all
all: clean checks unit-test

.PHONY: checks
checks: license lint
checks: generate license lint

.PHONY: lint
lint:
lint: generate
@scripts/check_lint.sh

.PHONY: license
license:
@scripts/check_license.sh

.PHONY: unit-test
unit-test:
unit-test: generate
@scripts/check_unit.sh

.PHONY: clean
clean:
@rm -rf ./.build
@rm -rf coverage*.out
@rm -rf coverage*.out

.PHONY: generate
generate:
@GOBIN=$(GOBIN_PATH) go install github.com/golang/mock/mockgen@$(MOCK_VERSION)
@go generate ./...

.PHONY: tidy-modules
tidy-modules:
@find . -type d \( -name build -prune \) -o -name go.mod -print | while read -r gomod_path; do \
dir_path=$$(dirname "$$gomod_path"); \
echo "Executing 'go mod tidy' in directory: $$dir_path"; \
(cd "$$dir_path" && GOPROXY=$(GOPROXY) go mod tidy) || exit 1; \
done
100 changes: 100 additions & 0 deletions cwt/cwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package cwt

import (
"errors"

"github.com/fxamacker/cbor/v2"
"github.com/veraison/go-cose"
)

const (
issuerPayloadIndex = 1
keyIDHeaderIndex = int64(4)
)

// SignParameters contains parameters of signing for cwt vc.
type SignParameters struct {
KeyID string
CWTAlg cose.Algorithm
}

// ParseAndCheckProof parses input JWT in serialized form into JSON Web Token and check signature proof.
// if checkIssuer set to true, will check if issuer set by "iss" own key set by "kid" header.
func ParseAndCheckProof(
cwtSerialized []byte,
proofChecker ProofChecker,
checkIssuer bool,
) (*cose.Sign1Message, []byte, error) {
cwtParsed, err := Parse(cwtSerialized)
if err != nil {
return nil, nil, err
}

var expectedProofIssuer *string

if checkIssuer {
payload := map[int]interface{}{}
if err = cbor.Unmarshal(cwtParsed.Payload, &payload); err != nil {
return nil, nil, err
}

iss, ok := payload[issuerPayloadIndex]
if !ok {
return nil, nil, errors.New("check cwt failure: iss claim is required")
}

issStr, ok := iss.(string)
if !ok {
return nil, nil, errors.New("check cwt failure: iss claim is not a string")
}

expectedProofIssuer = &issStr
}

err = CheckProof(cwtParsed, proofChecker, expectedProofIssuer)
if err != nil {
return nil, nil, err
}

return cwtParsed, cwtParsed.Payload, nil
}

// Parse parses input CWT in serialized form into JSON Web Token.
func Parse(cwtSerialized []byte) (*cose.Sign1Message, error) {
var message cose.Sign1Message
if err := message.UnmarshalCBOR(cwtSerialized); err != nil {
return nil, err
}

return &message, nil
}

// CheckProof checks that jwt have correct signature.
func CheckProof(
message *cose.Sign1Message,
proofChecker ProofChecker,
expectedProofIssuer *string,
) error {
alg, err := message.Headers.Protected.Algorithm()
if err != nil {
return err
}

keyIDBytes, ok := message.Headers.Unprotected[keyIDHeaderIndex].([]byte)
if !ok {
return errors.New("check cwt failure: kid header is required")
}

checker := Verifier{
ProofChecker: proofChecker,
expectedProofIssuer: expectedProofIssuer,
}

return checker.Verify(message, string(keyIDBytes), alg)
}
157 changes: 157 additions & 0 deletions cwt/cwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
Copyright Gen Digital Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package cwt_test

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
_ "crypto/sha256"
"encoding/hex"
"errors"
"testing"

"github.com/fxamacker/cbor/v2"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/veraison/go-cose"

"github.com/trustbloc/vc-go/cwt"
"github.com/trustbloc/vc-go/proof/checker"
)

const (
exampleCWT = "d28443a10126a104524173796d6d657472696345434453413235365850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6fa29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a52b9b63632c57209120e1c9e30" //nolint:lll
)

func TestParse(t *testing.T) {
t.Run("success", func(t *testing.T) {
input, decodeErr := hex.DecodeString(exampleCWT)
assert.NoError(t, decodeErr)

proofChecker := NewMockProofChecker(gomock.NewController(t))
proofChecker.EXPECT().CheckCWTProof(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(request checker.CheckCWTProofRequest, message *cose.Sign1Message, expectedIssuer string) error {
assert.Equal(t, "AsymmetricECDSA256", request.KeyID)
assert.Equal(t, cose.AlgorithmES256, request.Algo)
assert.NotNil(t, message)
assert.Equal(t, "coap://as.example.com", expectedIssuer)
return nil
})

resp, _, err := cwt.ParseAndCheckProof(input, proofChecker, true)
assert.NoError(t, err)
assert.NotNil(t, resp)
})

t.Run("invalid proof", func(t *testing.T) {
input, decodeErr := hex.DecodeString(exampleCWT)
assert.NoError(t, decodeErr)

proofChecker := NewMockProofChecker(gomock.NewController(t))
proofChecker.EXPECT().CheckCWTProof(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(request checker.CheckCWTProofRequest, message *cose.Sign1Message, expectedIssuer string) error {
return errors.New("invalid proof")
})

resp, _, err := cwt.ParseAndCheckProof(input, proofChecker, true)
assert.ErrorContains(t, err, "invalid proof")
assert.Nil(t, resp)
})

t.Run("invalid cwt", func(t *testing.T) {
resp, _, err := cwt.ParseAndCheckProof([]byte(exampleCWT), nil, true)
assert.ErrorContains(t, err, "invalid COSE_Sign1_Tagged object")
assert.Nil(t, resp)
})

t.Run("missing issuer", func(t *testing.T) {
data := map[int]interface{}{
100500: "1234567890",
}

encoded, err := cbor.Marshal(data)
assert.NoError(t, err)

signature, err := SignP256(encoded)
assert.NoError(t, err)

resp, _, err := cwt.ParseAndCheckProof(signature, nil, true)
assert.ErrorContains(t, err, "check cwt failure: iss claim is required")
assert.Nil(t, resp)
})

t.Run("issuer invalid type", func(t *testing.T) {
data := map[int]interface{}{
1: 100500,
}

encoded, err := cbor.Marshal(data)
assert.NoError(t, err)

signature, err := SignP256(encoded)
assert.NoError(t, err)

resp, _, err := cwt.ParseAndCheckProof(signature, nil, true)
assert.ErrorContains(t, err, "check cwt failure: iss claim is not a string")
assert.Nil(t, resp)
})

t.Run("invalid data type", func(t *testing.T) {
data := map[string]interface{}{
"100500": "1234567890",
}

encoded, err := cbor.Marshal(data)
assert.NoError(t, err)

signature, err := SignP256(encoded)
assert.NoError(t, err)

resp, _, err := cwt.ParseAndCheckProof(signature, nil, true)
assert.ErrorContains(t, err, "cbor: cannot unmarshal UTF-8 text string into Go value of type int")
assert.Nil(t, resp)
})

t.Run("no algo", func(t *testing.T) {
assert.ErrorContains(t, cwt.CheckProof(&cose.Sign1Message{}, nil, nil),
"algorithm not found")
})

t.Run("no key", func(t *testing.T) {
assert.ErrorContains(t, cwt.CheckProof(&cose.Sign1Message{
Headers: cose.Headers{
Protected: cose.ProtectedHeader{
cose.HeaderLabelAlgorithm: cose.AlgorithmES256,
},
},
}, nil, nil),
"check cwt failure: kid header is required")
})
}

func SignP256(data []byte) ([]byte, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}

signer, err := cose.NewSigner(cose.AlgorithmES256, privateKey)
if err != nil {
return nil, err
}

// create message header
headers := cose.Headers{
Protected: cose.ProtectedHeader{
cose.HeaderLabelAlgorithm: cose.AlgorithmES256,
},
}

// sign and marshal message
return cose.Sign1(rand.Reader, signer, headers, data, nil)
}
23 changes: 23 additions & 0 deletions cwt/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
Copyright Gen Digital Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package cwt

//go:generate mockgen -destination interfaces_mocks_test.go -package cwt_test -source=interfaces.go
import (
"github.com/veraison/go-cose"

"github.com/trustbloc/vc-go/proof/checker"
)

// ProofChecker used to check proof of jwt vc.
type ProofChecker interface {
CheckCWTProof(
checkCWTRequest checker.CheckCWTProofRequest,
msg *cose.Sign1Message,
expectedProofIssuer string,
) error
}
42 changes: 42 additions & 0 deletions cwt/wrappers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright Gen Digital Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package cwt

import (
"strings"

"github.com/veraison/go-cose"

"github.com/trustbloc/vc-go/proof/checker"
)

// Verifier verifies CWT proof.
type Verifier struct {
ProofChecker ProofChecker
expectedProofIssuer *string
}

// Verify verifies CWT proof.
func (v *Verifier) Verify(
proof *cose.Sign1Message,
keyID string,
algo cose.Algorithm,
) error {
var expectedProofIssuer string

if v.expectedProofIssuer != nil {
expectedProofIssuer = *v.expectedProofIssuer
} else {
// if expectedProofIssuer not set, we get issuer DID from first part of key id.
expectedProofIssuer = strings.Split(keyID, "#")[0]
}

return v.ProofChecker.CheckCWTProof(checker.CheckCWTProofRequest{
KeyID: keyID,
Algo: algo,
}, proof, expectedProofIssuer)
}
Loading
Loading