This repository has been archived by the owner on Mar 27, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add did configuration client (#3391)
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
Showing
3 changed files
with
333 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
}` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters