From 41999891f1c7a4cdd506569202eb1b008096fe87 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Fri, 22 Nov 2024 12:02:43 +0100 Subject: [PATCH] fix: did web resolver --- Makefile | 12 +++-- doc/did/doc.go | 47 ++++++++++++++---- doc/did/schema.go | 17 +++++-- doc/ld/proof/proof.go | 8 ++++ go.mod | 1 + go.sum | 5 ++ method/web/interfaces.go | 9 ++++ method/web/interfaces_mocks_test.go | 74 +++++++++++++++++++++++++++++ method/web/resolver_test.go | 34 +++++++++++++ method/web/testdata/uscis.json | 42 ++++++++++++++++ 10 files changed, 233 insertions(+), 16 deletions(-) create mode 100644 method/web/interfaces.go create mode 100644 method/web/interfaces_mocks_test.go create mode 100644 method/web/testdata/uscis.json diff --git a/Makefile b/Makefile index bc3d478..a38d92a 100644 --- a/Makefile +++ b/Makefile @@ -9,12 +9,13 @@ DOCKER_CMD ?= docker GOBIN_PATH=$(abspath .)/build/bin MOCKGEN=$(GOBIN_PATH)/mockgen GOMOCKS=pkg/internal/gomocks +MOCK_VERSION ?=v1.7.0-rc.1 .PHONY: all -all: clean checks unit-test +all: clean generate checks unit-test .PHONY: checks -checks: license lint +checks: generate license lint .PHONY: lint lint: @@ -31,4 +32,9 @@ unit-test: .PHONY: clean clean: @rm -rf ./.build - @rm -rf coverage*.out \ No newline at end of file + @rm -rf coverage*.out + +.PHONY: generate +generate: + @GOBIN=$(GOBIN_PATH) go install github.com/golang/mock/mockgen@$(MOCK_VERSION) + @go generate ./... diff --git a/doc/did/doc.go b/doc/did/doc.go index fbc074d..310983e 100644 --- a/doc/did/doc.go +++ b/doc/did/doc.go @@ -453,6 +453,36 @@ type rawDoc struct { Proof []interface{} `json:"proof,omitempty"` } +// UnmarshalJSON handles the custom unmarshaling logic for the rawDoc struct +func (r *rawDoc) UnmarshalJSON(data []byte) error { + type Alias rawDoc + temp := &struct { + Proof json.RawMessage `json:"proof,omitempty"` + *Alias + }{ + Alias: (*Alias)(r), + } + + if err := json.Unmarshal(data, temp); err != nil { + return err + } + + var proofSingle interface{} + var proofArray []interface{} + + if err := json.Unmarshal(temp.Proof, &proofSingle); err == nil { + proofArray = []interface{}{proofSingle} + } else { + if err = json.Unmarshal(temp.Proof, &proofArray); err != nil { + return fmt.Errorf("failed to unmarshal proof: %v", err) + } + } + + r.Proof = proofArray + + return nil +} + // Proof is cryptographic proof of the integrity of the DID Document. type Proof struct { Type string @@ -601,13 +631,6 @@ func populateProofs(context, didID, baseURI string, rawProofs []interface{}) ([] return nil, errors.New("rawProofs is not map[string]interface{}") } - created := stringEntry(emap[jsonldCreated]) - - timeValue, err := time.Parse(time.RFC3339, created) - if err != nil { - return nil, err - } - proofKey := jsonldProofValue if context == contextV011 { @@ -635,7 +658,6 @@ func populateProofs(context, didID, baseURI string, rawProofs []interface{}) ([] proof := Proof{ Type: stringEntry(emap[jsonldType]), - Created: &timeValue, Creator: creator, ProofValue: proofValue, ProofPurpose: stringEntry(emap[jsonldProofPurpose]), @@ -644,6 +666,15 @@ func populateProofs(context, didID, baseURI string, rawProofs []interface{}) ([] relativeURL: isRelative, } + created := stringEntry(emap[jsonldCreated]) + if created != "" { + if timeValue, errTime := time.Parse(time.RFC3339, created); errTime != nil { + return nil, errTime + } else { + proof.Created = &timeValue + } + } + proofs = append(proofs, proof) } diff --git a/doc/did/schema.go b/doc/did/schema.go index c366b2b..745e68a 100644 --- a/doc/did/schema.go +++ b/doc/did/schema.go @@ -89,17 +89,24 @@ const ( "updated": { "type": "string" }, - "proof": { - "type": "array", - "items": { + "proof": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/proof" + } + }, + { "$ref": "#/definitions/proof" } - } + ] + } }, "definitions": { "proof": { "type": "object", - "required": [ "type", "creator", "created", "proofValue"], + "required": [ "type", "proofValue"], "properties": { "type": { "type": "string", diff --git a/doc/ld/proof/proof.go b/doc/ld/proof/proof.go index 9244dc7..7001069 100644 --- a/doc/ld/proof/proof.go +++ b/doc/ld/proof/proof.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "fmt" + "strings" "github.com/multiformats/go-multibase" @@ -158,6 +159,13 @@ func DecodeProofValue(s, proofType string) ([]byte, error) { return nil, errors.New("unsupported encoding") } + if strings.HasPrefix(s, "z") { // maybe base58 + _, value, err := multibase.Decode(s) + if err == nil { + return value, nil + } + } + return decodeBase64(s) } diff --git a/go.mod b/go.mod index 5dc6b30..93ed69e 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce github.com/cenkalti/backoff/v4 v4.1.3 github.com/go-jose/go-jose/v3 v3.0.1 + github.com/golang/mock v1.4.4 github.com/google/uuid v1.3.0 github.com/mitchellh/mapstructure v1.5.0 github.com/multiformats/go-multibase v0.1.1 diff --git a/go.sum b/go.sum index 36a1555..48e5c01 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -104,8 +106,10 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -113,6 +117,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/method/web/interfaces.go b/method/web/interfaces.go new file mode 100644 index 0000000..13d8062 --- /dev/null +++ b/method/web/interfaces.go @@ -0,0 +1,9 @@ +package web + +import "net/http" + +//go:generate mockgen -destination interfaces_mocks_test.go -package web -source=interfaces.go + +type roundTripper interface { //nolint + http.RoundTripper +} diff --git a/method/web/interfaces_mocks_test.go b/method/web/interfaces_mocks_test.go new file mode 100644 index 0000000..39d86cf --- /dev/null +++ b/method/web/interfaces_mocks_test.go @@ -0,0 +1,74 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces.go + +// Package web is a generated GoMock package. +package web + +import ( + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockroundTripper is a mock of roundTripper interface. +type MockroundTripper struct { + ctrl *gomock.Controller + recorder *MockroundTripperMockRecorder +} + +// MockroundTripperMockRecorder is the mock recorder for MockroundTripper. +type MockroundTripperMockRecorder struct { + mock *MockroundTripper +} + +// NewMockroundTripper creates a new mock instance. +func NewMockroundTripper(ctrl *gomock.Controller) *MockroundTripper { + mock := &MockroundTripper{ctrl: ctrl} + mock.recorder = &MockroundTripperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockroundTripper) EXPECT() *MockroundTripperMockRecorder { + return m.recorder +} + +// RoundTrip mocks base method. +func (m *MockroundTripper) RoundTrip(arg0 *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RoundTrip", arg0) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RoundTrip indicates an expected call of RoundTrip. +func (mr *MockroundTripperMockRecorder) RoundTrip(arg0 interface{}) *roundTripperRoundTripCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoundTrip", reflect.TypeOf((*MockroundTripper)(nil).RoundTrip), arg0) + return &roundTripperRoundTripCall{Call: call} +} + +// roundTripperRoundTripCall wrap *gomock.Call +type roundTripperRoundTripCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *roundTripperRoundTripCall) Return(arg0 *http.Response, arg1 error) *roundTripperRoundTripCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *roundTripperRoundTripCall) Do(f func(*http.Request) (*http.Response, error)) *roundTripperRoundTripCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *roundTripperRoundTripCall) DoAndReturn(f func(*http.Request) (*http.Response, error)) *roundTripperRoundTripCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/method/web/resolver_test.go b/method/web/resolver_test.go index 508155a..aa1425a 100644 --- a/method/web/resolver_test.go +++ b/method/web/resolver_test.go @@ -7,7 +7,10 @@ SPDX-License-Identifier: Apache-2.0 package web import ( + "bytes" + _ "embed" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" @@ -15,6 +18,7 @@ import ( "strings" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" didapi "github.com/trustbloc/did-go/doc/did" @@ -42,6 +46,9 @@ const ( invalidDoc = `{}` ) +//go:embed testdata/uscis.json +var uscisDid []byte + func TestParseDID(t *testing.T) { t.Run("test parse did success", func(t *testing.T) { address, host, err := parseDIDWeb(validDID, false) @@ -219,3 +226,30 @@ func TestResolveWebFixtures(t *testing.T) { require.Equal(t, expectedDoc, docResolution.DIDDocument) }) } + +func TestResolveUscisDid(t *testing.T) { + cl := &http.Client{} + trip := NewMockroundTripper(gomock.NewController(t)) + cl.Transport = trip + + trip.EXPECT().RoundTrip(gomock.Any()). + DoAndReturn(func(request *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(uscisDid)), + }, nil + }) + + v := New() + + docResolution, err := v.Read("did:web:dhs-svip.github.io:ns:uscis:oidp", + vdrapi.WithOption(HTTPClientOpt, cl)) + + require.NoError(t, err) + require.NotNil(t, docResolution) + + require.Len(t, docResolution.DIDDocument.Proof, 1) + require.Len(t, docResolution.DIDDocument.VerificationMethod, 2) + require.EqualValues(t, "did:web:dhs-svip.github.io:ns:uscis:oidp", docResolution.DIDDocument.ID) + require.NotEmpty(t, docResolution.DIDDocument.Proof[0].ProofValue) +} diff --git a/method/web/testdata/uscis.json b/method/web/testdata/uscis.json new file mode 100644 index 0000000..2fb242b --- /dev/null +++ b/method/web/testdata/uscis.json @@ -0,0 +1,42 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/data-integrity/v2" + ], + "id": "did:web:dhs-svip.github.io:ns:uscis:oidp", + "verificationMethod": [ + { + "id": "did:web:dhs-svip.github.io:ns:uscis:oidp#zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd", + "type": "Multikey", + "controller": "did:web:dhs-svip.github.io:ns:uscis:oidp", + "publicKeyMultibase": "zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd" + }, + { + "id": "did:web:dhs-svip.github.io:ns:uscis:oidp#zUC7JawkqoPt9G87naub7Qbes4F9JWXJDgmbGHhZsfDYmoHVmDqZCE6rTTWiXGhFMjUEfJi1xxmDM1mga35Bue5zCp9aZp8bNFiztkFbHUtULb2PmWWvhfadVu3YfxuWR2s3z3e", + "type": "Multikey", + "controller": "did:web:dhs-svip.github.io:ns:uscis:oidp", + "publicKeyMultibase": "zUC7JawkqoPt9G87naub7Qbes4F9JWXJDgmbGHhZsfDYmoHVmDqZCE6rTTWiXGhFMjUEfJi1xxmDM1mga35Bue5zCp9aZp8bNFiztkFbHUtULb2PmWWvhfadVu3YfxuWR2s3z3e" + } + ], + "authentication": [ + "did:web:dhs-svip.github.io:ns:uscis:oidp#zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd" + ], + "assertionMethod": [ + "did:web:dhs-svip.github.io:ns:uscis:oidp#zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd", + "did:web:dhs-svip.github.io:ns:uscis:oidp#zUC7JawkqoPt9G87naub7Qbes4F9JWXJDgmbGHhZsfDYmoHVmDqZCE6rTTWiXGhFMjUEfJi1xxmDM1mga35Bue5zCp9aZp8bNFiztkFbHUtULb2PmWWvhfadVu3YfxuWR2s3z3e" + ], + "capabilityDelegation": [ + "did:web:dhs-svip.github.io:ns:uscis:oidp#zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd" + ], + "capabilityInvocation": [ + "did:web:dhs-svip.github.io:ns:uscis:oidp#zDnaekqKLkVN1HqzBxy1Ti8niyCRxWkKr6cxDvX6P4qXDBATd" + ], + "proof": { + "type": "DataIntegrityProof", + "verificationMethod": "did:key:zDnaemaR1s6YRSbkzwFPUF7AZgzLh2hscEkEy1QS5XojpmZmA#zDnaemaR1s6YRSbkzwFPUF7AZgzLh2hscEkEy1QS5XojpmZmA", + "cryptosuite": "ecdsa-rdfc-2019", + "proofPurpose": "assertionMethod", + "proofValue": "z52Hq5miF9FmksiRTfQJGz2pntJJWAZcu927MNCLPMUtMiDaq8AL8DkVwxXQ5f9dcGd9gipbmWt5dLVEcPgdsNXta" + } +}