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

Commit

Permalink
Credential Verification & DID Resolution (#121)
Browse files Browse the repository at this point in the history
* init

* temp

* update sdk

* add did resolver

* cred verification

* working but untested

* did resolution api and tests

* bug

* update ci

* verification debugging

* working

* pr comments
  • Loading branch information
Gabe authored Oct 10, 2022
1 parent 7618f44 commit 556edf7
Show file tree
Hide file tree
Showing 31 changed files with 784 additions and 118 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.19.2

- name: Install Mage
run: go install github.com/magefile/mage
Expand All @@ -35,7 +35,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.19.2

- name: Install mage
run: go install github.com/magefile/mage
Expand Down
1 change: 1 addition & 0 deletions config/compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ password = "default-password"
[services.did]
name = "did"
methods = ["key"]
resolution_methods = ["key", "web", "pkh", "peer"]

[services.schema]
name = "schema"
Expand Down
4 changes: 3 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool {

type DIDServiceConfig struct {
*BaseServiceConfig
Methods []string `toml:"methods"`
Methods []string `toml:"methods"`
ResolutionMethods []string `toml:"resolution_methods"`
}

func (d *DIDServiceConfig) IsEmpty() bool {
Expand Down Expand Up @@ -184,6 +185,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
DIDConfig: DIDServiceConfig{
BaseServiceConfig: &BaseServiceConfig{Name: "did"},
Methods: []string{"key"},
ResolutionMethods: []string{"key", "peer", "web", "pkh"},
},
SchemaConfig: SchemaServiceConfig{
BaseServiceConfig: &BaseServiceConfig{Name: "schema"},
Expand Down
1 change: 1 addition & 0 deletions config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ password = "default-password"
[services.did]
name = "did"
methods = ["key"]
resolution_methods = ["key", "web", "pkh", "peer"]

[services.schema]
name = "schema"
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.19

require (
github.com/BurntSushi/toml v1.2.0
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221001002612-ec8641015c21
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007180051-d48966db0f15
github.com/ardanlabs/conf v1.5.0
github.com/dimfeld/httptreemux/v5 v5.4.0
github.com/go-playground/locales v0.14.0
Expand All @@ -21,7 +21,7 @@ require (
go.opentelemetry.io/otel/exporters/jaeger v1.10.0
go.opentelemetry.io/otel/sdk v1.10.0
go.opentelemetry.io/otel/trace v1.10.0
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
gopkg.in/go-playground/validator.v9 v9.31.0
)

Expand Down Expand Up @@ -51,7 +51,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220921215642-f749c4d89b8b h1:0IJ
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220921215642-f749c4d89b8b/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221001002612-ec8641015c21 h1:tsvfXXVW2rqdPIYJ5uLSUhTXG4C6niB9FmjpgQE/f9w=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221001002612-ec8641015c21/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221006024829-0be992d222cd h1:TfikEZGgOY0nSlJ3GlIBLisMu9zkV0cIXg+dbXX4poI=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221006024829-0be992d222cd/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007010336-560f7ccb3b93 h1:paLRTB3VnfKfcJgx43faOgVM2ypKFJDrvovq5/BykL0=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007010336-560f7ccb3b93/go.mod h1:oi28ndJG+ksI/NvGI+x7t6xHiDXYGOYxORTjuVynklQ=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007180051-d48966db0f15 h1:tg8HA0K2/a7X6K/K5ayToC7Yrout5+v2CZH5sGpX2mc=
github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007180051-d48966db0f15/go.mod h1:oi28ndJG+ksI/NvGI+x7t6xHiDXYGOYxORTjuVynklQ=
github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c=
github.com/ardanlabs/conf v1.5.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand Down Expand Up @@ -130,6 +136,8 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -141,6 +149,8 @@ golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3B
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down
188 changes: 188 additions & 0 deletions internal/credential/verification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package credential

import (
"crypto"
"fmt"

credsdk "github.com/TBD54566975/ssi-sdk/credential"
"github.com/TBD54566975/ssi-sdk/credential/signing"
"github.com/TBD54566975/ssi-sdk/credential/verification"
"github.com/TBD54566975/ssi-sdk/cryptosuite"
didsdk "github.com/TBD54566975/ssi-sdk/did"
"github.com/goccy/go-json"
"github.com/lestrrat-go/jwx/jwk"
"github.com/mr-tron/base58"
"github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/tbd54566975/ssi-service/internal/keyaccess"
"github.com/tbd54566975/ssi-service/internal/util"
)

type CredentialVerifier struct {
verifier *verification.CredentialVerifier
resolver *didsdk.Resolver
}

// NewCredentialVerifier creates a new credential verifier which executes both signature and static verification checks.
// In the future the set of verification checks will be configurable.
func NewCredentialVerifier(resolver *didsdk.Resolver) (*CredentialVerifier, error) {
if resolver == nil {
return nil, errors.New("resolver is nil")
}
// TODO(gabe): consider making this configurable
verifiers := verification.KnownVerifiers
verifier, err := verification.NewCredentialVerifier(verifiers)
if err != nil {
return nil, errors.Wrap(err, "failed to create static credential verifier")
}
return &CredentialVerifier{
verifier: verifier,
resolver: resolver,
}, nil
}

// TODO(gabe) consider moving this verification logic to the sdk https://github.com/TBD54566975/ssi-service/issues/122

// VerifyJWTCredential first parses and checks the signature on the given JWT credential. Next, it runs
// a set of static verification checks on the credential as per the credential service's configuration.
func (v CredentialVerifier) VerifyJWTCredential(token string) error {
cred, err := signing.ParseVerifiableCredentialFromJWT(token)
if err != nil {
return util.LoggingErrorMsg(err, "could not parse credential from JWT")
}
kid, pubKey, err := v.getVerificationInformation(cred.Issuer.(string), "")
if err != nil {
return util.LoggingErrorMsg(err, "could not get verification from credential")
}
verifier, err := keyaccess.NewJWKKeyAccessVerifier(kid, pubKey)
if err != nil {
return util.LoggingErrorMsg(err, "could not create verifier")
}
if err := verifier.Verify(keyaccess.JWKToken{Token: token}); err != nil {
return util.LoggingErrorMsg(err, "could not verify credential's signature")
}
if err = v.verifier.VerifyCredential(*cred); err != nil {
return util.LoggingErrorMsg(err, "static credential verification failed")
}
return err
}

// VerifyDataIntegrityCredential first checks the signature on the given data integrity credential. Next, it runs
// a set of static verification checks on the credential as per the credential service's configuration.
func (v CredentialVerifier) VerifyDataIntegrityCredential(credential credsdk.VerifiableCredential) error {
// TODO(gabe): perhaps this should be a verification method referenced on the proof object, not the issuer
kid, pubKey, err := v.getVerificationInformation(credential.Issuer.(string), "")
if err != nil {
return util.LoggingErrorMsg(err, "could not get verification from credential")
}
verifier, err := keyaccess.NewDataIntegrityKeyAccess(kid, pubKey)
if err != nil {
return util.LoggingErrorMsg(err, "could not create verifier")
}
if err := verifier.Verify(&credential); err != nil {
return util.LoggingErrorMsg(err, "could not verify credential's signature")
}
if err = v.verifier.VerifyCredential(credential); err != nil {
return util.LoggingErrorMsg(err, "static credential verification failed")
}
return err
}

// getVerificationInformation resolves a DID and provides a kid and public key needed for credential verification
// it is possible that a DID has multiple verification methods, in which case a kid must be provided, otherwise
// resolution will fail.
func (v CredentialVerifier) getVerificationInformation(did, maybeKID string) (kid string, pubKey crypto.PublicKey, err error) {
resolved, err := v.resolver.Resolve(did)
if err != nil {
return "", nil, errors.Wrapf(err, "failed to resolve did: %s", did)
}
if resolved.DIDDocument.IsEmpty() {
return "", nil, errors.Errorf("did doc: %s is empty", did)
}
verificationMethods := resolved.DIDDocument.VerificationMethod
if len(verificationMethods) == 0 {
return "", nil, errors.Errorf("did doc: %s has no verification methods", did)
}

// handle the case where a kid is provided && there are multiple verification methods
if len(verificationMethods) > 1 {
if kid == "" {
return "", nil, errors.Errorf("kid is required for did: %s, which has multiple verification methods", did)
}
for _, method := range verificationMethods {
if method.ID == kid {
kid = did
pubKey, err = extractKeyFromVerificationMethod(verificationMethods[0])
return
}
}
}
// TODO(gabe): some DIDs, like did:key have KIDs that aren't used, so we need to know when to use a kid vs the DID
kid = did
pubKey, err = extractKeyFromVerificationMethod(verificationMethods[0])
return
}

func extractKeyFromVerificationMethod(method didsdk.VerificationMethod) (pubKey crypto.PublicKey, err error) {
if method.PublicKeyMultibase != "" {
pubKeyBytes, multiBaseErr := multibaseToPubKeyBytes(method.PublicKeyMultibase)
if multiBaseErr != nil {
err = multiBaseErr
return
}
pubKey, err = cryptosuite.PubKeyBytesToTypedKey(pubKeyBytes, method.Type)
return
} else if method.PublicKeyBase58 != "" {
pubKeyDecoded, b58Err := base58.Decode(method.PublicKeyBase58)
if b58Err != nil {
err = b58Err
return
}
pubKey, err = cryptosuite.PubKeyBytesToTypedKey(pubKeyDecoded, method.Type)
return
} else if method.PublicKeyJWK != nil {
jwkBytes, jwkErr := json.Marshal(method.PublicKeyJWK)
if err != nil {
err = jwkErr
return
}
pubKey, err = jwk.ParseKey(jwkBytes)
return
}
err = errors.New("no public key found in verification method")
return
}

// multibaseToPubKey converts a multibase encoded public key to public key bytes for known multibase encodings
func multibaseToPubKeyBytes(mb string) ([]byte, error) {
if mb == "" {
err := fmt.Errorf("could not decode value: %s", mb)
logrus.WithError(err).Error()
return nil, err
}

encoding, decoded, err := multibase.Decode(mb)
if err != nil {
logrus.WithError(err).Error("could not decode did:key")
return nil, err
}
if encoding != didsdk.Base58BTCMultiBase {
err := fmt.Errorf("expected %d encoding but found %d", didsdk.Base58BTCMultiBase, encoding)
logrus.WithError(err).Error()
return nil, err
}

// n = # bytes for the int, which we expect to be two from our multicodec
_, n, err := varint.FromUvarint(decoded)
if err != nil {
return nil, err
}
if n != 2 {
return nil, errors.New("error parsing did:key varint")
}
pubKeyBytes := decoded[n:]
return pubKeyBytes, nil
}
42 changes: 42 additions & 0 deletions internal/did/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package did

import (
"fmt"

didsdk "github.com/TBD54566975/ssi-sdk/did"
"github.com/pkg/errors"
)

// BuildResolver builds a DID resolver from a list of methods to support resolution for
func BuildResolver(methods []string) (*didsdk.Resolver, error) {
if len(methods) == 0 {
return nil, errors.New("no methods provided")
}
var resolvers []didsdk.Resolution
for _, method := range methods {
resolver, err := getKnownResolver(method)
if err != nil {
return nil, err
}
resolvers = append(resolvers, resolver)
}
if len(resolvers) == 0 {
return nil, errors.New("no resolvers created")
}
return didsdk.NewResolver(resolvers...)
}

// all possible resolvers for the DID service
func getKnownResolver(method string) (didsdk.Resolution, error) {
switch didsdk.Method(method) {
case didsdk.KeyMethod:
return new(didsdk.KeyResolver), nil
case didsdk.WebMethod:
return new(didsdk.WebResolver), nil
case didsdk.PKHMethod:
return new(didsdk.PKHResolver), nil
case didsdk.PeerMethod:
return new(didsdk.PeerResolver), nil
}
return nil, fmt.Errorf("unsupported method: %s", method)
}
32 changes: 32 additions & 0 deletions internal/did/resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package did

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestResolver(t *testing.T) {
// empty resolver
_, err := BuildResolver(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no methods provided")

// unsupported method
_, err = BuildResolver([]string{"unsupported"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported method: unsupported")

// valid method
resolver, err := BuildResolver([]string{"key"})
assert.NoError(t, err)
assert.NotEmpty(t, resolver)
resolved, err := resolver.Resolve("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp")
assert.NoError(t, err)
assert.NotEmpty(t, resolved)

// resolve for a method that is not supported
_, err = resolver.Resolve("did:web:example.com")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported method: web")
}
Loading

0 comments on commit 556edf7

Please sign in to comment.