This repository has been archived by the owner on Dec 12, 2024. It is now read-only.
generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Credential Verification & DID Resolution (#121)
* 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
Showing
31 changed files
with
784 additions
and
118 deletions.
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
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
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
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
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
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
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,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 | ||
} |
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,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) | ||
} |
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,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") | ||
} |
Oops, something went wrong.