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

Schema Signing & Verification #123

Merged
merged 9 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 14 additions & 110 deletions internal/credential/verification.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
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"
Expand Down Expand Up @@ -53,7 +43,12 @@ func (v CredentialVerifier) VerifyJWTCredential(token string) error {
if err != nil {
return util.LoggingErrorMsg(err, "could not parse credential from JWT")
}
kid, pubKey, err := v.getVerificationInformation(cred.Issuer.(string), "")
issuerDID := cred.Issuer.(string)
resolved, err := v.resolver.Resolve(issuerDID)
if err != nil {
return errors.Wrapf(err, "failed to resolve did: %s", issuerDID)
}
kid, pubKey, err := keyaccess.GetVerificationInformation(resolved.DIDDocument, "")
if err != nil {
return util.LoggingErrorMsg(err, "could not get verification from credential")
}
Expand All @@ -74,115 +69,24 @@ func (v CredentialVerifier) VerifyJWTCredential(token string) error {
// 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), "")
issuerDID := credential.Issuer.(string)
resolved, err := v.resolver.Resolve(issuerDID)
if err != nil {
return util.LoggingErrorMsg(err, "could not get verification from credential")
return errors.Wrapf(err, "failed to resolve did: %s", issuerDID)
}
kid, pubKey, err := keyaccess.GetVerificationInformation(resolved.DIDDocument, "")
if err != nil {
return util.LoggingErrorMsg(err, "could not get verification information 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")
return util.LoggingErrorMsg(err, "could not verify the 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
}
108 changes: 108 additions & 0 deletions internal/keyaccess/verification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package keyaccess
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this to its own package, since it has use outside of just cred verification


import (
"crypto"
"fmt"

"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"
)

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

// 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.ID)
}
for _, method := range verificationMethods {
if method.ID == kid {
kid = did.ID
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.ID
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
}
5 changes: 5 additions & 0 deletions pkg/server/framework/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"reflect"
"strings"

"github.com/TBD54566975/ssi-sdk/util"
"github.com/goccy/go-json"

"github.com/dimfeld/httptreemux/v5"
Expand Down Expand Up @@ -95,3 +96,7 @@ func Decode(r *http.Request, val interface{}) error {

return nil
}

func ValidateRequest(request interface{}) error {
return util.IsValidStruct(request)
}
9 changes: 8 additions & 1 deletion pkg/server/router/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,15 @@ type CreateCredentialResponse struct {
// @Router /v1/credentials [put]
func (cr CredentialRouter) CreateCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
var request CreateCredentialRequest
invalidCreateCredentialRequest := "invalid create credential request"
if err := framework.Decode(r, &request); err != nil {
errMsg := "invalid create credential request"
errMsg := invalidCreateCredentialRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

if err := framework.ValidateRequest(request); err != nil {
errMsg := invalidCreateCredentialRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/server/router/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,15 @@ func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter
}

var request CreateDIDByMethodRequest
invalidCreateDIDRequest := "invalid create DID request"
if err := framework.Decode(r, &request); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternativly you could do

if framework.Decode(r, &request) != nil || framework.ValidateRequest(request) != nil {
errMsg := invalidCreateDIDRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

since its the same output (but you need to get err variable in a golang way somehow lol maybe not possible)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, unfortunately this wouldn't work because both return unique errors and we couldn't capture both. conditional error assignment would be a nice feature 🤔

errMsg := "invalid create DID request"
errMsg := invalidCreateDIDRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

if err := framework.ValidateRequest(request); err != nil {
errMsg := invalidCreateDIDRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}
Expand Down
2 changes: 0 additions & 2 deletions pkg/server/router/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,11 @@ func TestManifestRouter(t *testing.T) {
createApplicationRequest := getValidApplicationRequest(applicantDID.DID.ID, createdManifest.Manifest.ID, createManifestRequest.Manifest.PresentationDefinition.InputDescriptors[0].ID)

createdApplicationResponse, err := manifestService.ProcessApplicationSubmission(createApplicationRequest)

assert.NoError(tt, err)
assert.NotEmpty(tt, createdManifest)
assert.NotEmpty(tt, createdApplicationResponse.Response.ID)
assert.Equal(tt, len(createManifestRequest.Manifest.OutputDescriptors), len(createdApplicationResponse.Credential))
})

}

func getValidManifestRequest(issuerDID string) manifest.CreateManifestRequest {
Expand Down
Loading