Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

Commit

Permalink
feat: Add did configuration client (#3391)
Browse files Browse the repository at this point in the history
Did configuration client will:
- retrieve did configuration from domain
- verify requested did and domain against did configuration

Closes #3390

Signed-off-by: Sandra Vrtikapa <[email protected]>

Signed-off-by: Sandra Vrtikapa <[email protected]>
  • Loading branch information
sandrask authored Sep 30, 2022
1 parent 2aee6d4 commit 5d09324
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 1 deletion.
111 changes: 111 additions & 0 deletions pkg/client/didconfig/didconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package didconfig

import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"

jsonld "github.com/piprate/json-gold/ld"

"github.com/hyperledger/aries-framework-go/pkg/common/log"
"github.com/hyperledger/aries-framework-go/pkg/doc/didconfig"
vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
)

var logger = log.New("aries-framework/client/did-config")

const defaultTimeout = time.Minute

// Client is a JSON-LD SDK client.
type Client struct {
httpClient HTTPClient
didConfigOpts []didconfig.DIDConfigurationOpt
}

// New creates new did configuration client.
func New(opts ...Option) *Client {
client := &Client{
httpClient: &http.Client{Timeout: defaultTimeout},
}

for _, opt := range opts {
opt(client)
}

return client
}

// HTTPClient represents an HTTP client.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}

// Option configures the did configuration client.
type Option func(opts *Client)

// WithHTTPClient option is for custom http client.
func WithHTTPClient(httpClient HTTPClient) Option {
return func(opts *Client) {
opts.httpClient = httpClient
}
}

// WithJSONLDDocumentLoader defines a JSON-LD document loader.
func WithJSONLDDocumentLoader(documentLoader jsonld.DocumentLoader) Option {
return func(opts *Client) {
opts.didConfigOpts = append(opts.didConfigOpts, didconfig.WithJSONLDDocumentLoader(documentLoader))
}
}

// WithVDRegistry defines a vdr service.
func WithVDRegistry(vdrRegistry vdrapi.Registry) Option {
return func(opts *Client) {
opts.didConfigOpts = append(opts.didConfigOpts, didconfig.WithVDRegistry(vdrRegistry))
}
}

// VerifyDIDAndDomain will verify that there is valid domain linkage credential in did configuration
// for specified did and domain.
func (c *Client) VerifyDIDAndDomain(did, domain string) error {
endpoint := domain + "/.well-known/did-configuration.json"

req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil)
if err != nil {
return fmt.Errorf("new HTTP request: %w", err)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("httpClient.Do: %w", err)
}

defer closeResponseBody(resp.Body)

responseBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("endpoint %s returned status '%d' and message '%s'",
endpoint, resp.StatusCode, responseBytes)
}

return didconfig.VerifyDIDAndDomain(responseBytes, did, domain)
}

func closeResponseBody(respBody io.Closer) {
e := respBody.Close()
if e != nil {
logger.Warnf("failed to close response body: %v", e)
}
}
221 changes: 221 additions & 0 deletions pkg/client/didconfig/didconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package didconfig

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"

"github.com/stretchr/testify/require"

"github.com/hyperledger/aries-framework-go/pkg/doc/ldcontext"
"github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil"
"github.com/hyperledger/aries-framework-go/pkg/vdr"
"github.com/hyperledger/aries-framework-go/pkg/vdr/key"
)

const (
testDID = "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM"
testDomain = "https://identity.foundation"

contextV1 = "https://identity.foundation/.well-known/did-configuration/v1"
)

func TestNew(t *testing.T) {
t.Run("success - default options", func(t *testing.T) {
c := New()
require.NotNil(t, c)
require.Len(t, c.didConfigOpts, 0)
})

t.Run("success - did config options provided", func(t *testing.T) {
loader, err := ldtestutil.DocumentLoader(ldcontext.Document{
URL: contextV1,
Content: json.RawMessage(didCfgCtxV1),
})
require.NoError(t, err)

c := New(WithJSONLDDocumentLoader(loader),
WithVDRegistry(vdr.New(vdr.WithVDR(key.New()))),
WithHTTPClient(&http.Client{}))
require.NotNil(t, c)
require.Len(t, c.didConfigOpts, 2)
})
}

func TestVerifyDIDAndDomain(t *testing.T) {
loader, err := ldtestutil.DocumentLoader(ldcontext.Document{
URL: contextV1,
Content: json.RawMessage(didCfgCtxV1),
})
require.NoError(t, err)

t.Run("success", func(t *testing.T) {
httpClient := &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(didCfg))),
}, nil
},
}

c := New(WithJSONLDDocumentLoader(loader), WithHTTPClient(httpClient))

err := c.VerifyDIDAndDomain(testDID, testDomain)
require.NoError(t, err)
})

t.Run("success", func(t *testing.T) {
httpClient := &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(didCfg))),
}, nil
},
}

c := New(WithJSONLDDocumentLoader(loader), WithHTTPClient(httpClient))

err := c.VerifyDIDAndDomain(testDID, testDomain)
require.NoError(t, err)
})

t.Run("error - http client error", func(t *testing.T) {
c := New(WithJSONLDDocumentLoader(loader))

err := c.VerifyDIDAndDomain(testDID, "https://non-existent-abc.com")
require.Error(t, err)
require.Contains(t, err.Error(),
"Get \"https://non-existent-abc.com/.well-known/did-configuration.json\": dial tcp: "+
"lookup non-existent-abc.com: no such host")
})

t.Run("error - http request error", func(t *testing.T) {
c := New(WithJSONLDDocumentLoader(loader))

err := c.VerifyDIDAndDomain(testDID, ":invalid.com")
require.Error(t, err)
require.Contains(t, err.Error(), "missing protocol scheme")
})

t.Run("error - http status error", func(t *testing.T) {
httpClient := &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: ioutil.NopCloser(bytes.NewReader([]byte("data not found"))),
}, nil
},
}

c := New(WithJSONLDDocumentLoader(loader), WithHTTPClient(httpClient))

err := c.VerifyDIDAndDomain(testDID, testDomain)
require.Error(t, err)
require.Contains(t, err.Error(), "endpoint https://identity.foundation/.well-known/did-configuration.json "+
"returned status '404' and message 'data not found'")
})

t.Run("error - did configuration missing linked DIDs", func(t *testing.T) {
httpClient := &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(didCfgNoLinkedDIDs))),
}, nil
},
}

c := New(WithJSONLDDocumentLoader(loader),
WithVDRegistry(vdr.New(vdr.WithVDR(key.New()))),
WithHTTPClient(httpClient))

err := c.VerifyDIDAndDomain(testDID, testDomain)
require.Error(t, err)
require.Contains(t, err.Error(), "did configuration: property 'linked_dids' is required ")
})
}

func TestCloseResponseBody(t *testing.T) {
t.Run("error", func(t *testing.T) {
closeResponseBody(&mockCloser{Err: fmt.Errorf("test error")})
})
}

type mockHTTPClient struct {
DoFunc func(req *http.Request) (*http.Response, error)
}

func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.DoFunc(req)
}

type mockCloser struct {
Err error
}

func (c *mockCloser) Close() error {
return c.Err
}

// nolint: lll
const didCfg = `
{
"@context": "https://identity.foundation/.well-known/did-configuration/v1",
"linked_dids": [
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://identity.foundation/.well-known/did-configuration/v1"
],
"issuer": "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM",
"issuanceDate": "2020-12-04T14:08:28-06:00",
"expirationDate": "2025-12-04T14:08:28-06:00",
"type": [
"VerifiableCredential",
"DomainLinkageCredential"
],
"credentialSubject": {
"id": "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM",
"origin": "https://identity.foundation"
},
"proof": {
"type": "Ed25519Signature2018",
"created": "2020-12-04T20:08:28.540Z",
"jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..D0eDhglCMEjxDV9f_SNxsuU-r3ZB9GR4vaM9TYbyV7yzs1WfdUyYO8rFZdedHbwQafYy8YOpJ1iJlkSmB4JaDQ",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM#z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM"
}
}
]
}`

const didCfgNoLinkedDIDs = `
{
"@context": "https://identity.foundation/.well-known/did-configuration/v1"
}`

// nolint: lll
const didCfgCtxV1 = `
{
"@context": [
{
"@version": 1.1,
"@protected": true,
"LinkedDomains": "https://identity.foundation/.well-known/resources/did-configuration/#LinkedDomains",
"DomainLinkageCredential": "https://identity.foundation/.well-known/resources/did-configuration/#DomainLinkageCredential",
"origin": "https://identity.foundation/.well-known/resources/did-configuration/#origin",
"linked_dids": "https://identity.foundation/.well-known/resources/did-configuration/#linked_dids"
}
]
}`
2 changes: 1 addition & 1 deletion pkg/doc/didconfig/didconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/hyperledger/aries-framework-go/pkg/vdr/key"
)

var logger = log.New("aries-framework/doc/verifiable")
var logger = log.New("aries-framework/doc/didconfig")

const (
// ContextV0 is did configuration context version 0.
Expand Down

0 comments on commit 5d09324

Please sign in to comment.