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

Add an endpoint to verify presentations #630

Merged
merged 13 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
60 changes: 58 additions & 2 deletions doc/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,23 @@ definitions:
description: Whether the credential was verified.
type: boolean
type: object
pkg_server_router.VerifyPresentationRequest:
properties:
presentationJwt:
description: A JWT that encodes a verifiable presentation according to https://www.w3.org/TR/vc-data-model/#json-web-token
type: string
required:
- presentationJwt
type: object
pkg_server_router.VerifyPresentationResponse:
properties:
reason:
description: The reason why this presentation couldn't be verified.
type: string
verified:
description: Whether the presentation was verified.
type: boolean
type: object
rendering.ColorResource:
properties:
color:
Expand Down Expand Up @@ -2517,10 +2534,10 @@ paths:
consumes:
- application/json
description: |-
Verify a given Verifiable Credential by its ID. The system does the following levels of verification:
Verifies a given verifiable credential. The system does the following levels of verification:
1. Makes sure the credential has a valid signature
2. Makes sure the credential has is not expired
3. Makes sure the credential complies with the VC Data Model
3. Makes sure the credential complies with the VC Data Model v1.1
4. If the credential has a schema, makes sure its data complies with the schema
parameters:
- description: request body
Expand Down Expand Up @@ -3886,6 +3903,45 @@ paths:
summary: Review a pending Presentation Submission
tags:
- PresentationSubmissions
/v1/presentations/verification:
put:
consumes:
- application/json
description: |-
Verifies a given presentation. The system does the following levels of verification:
1. Makes sure the presentation has a valid signature
2. Makes sure the presentation is not expired
3. Makes sure the presentation complies with https://www.w3.org/TR/vc-data-model/#presentations-0 of VC Data Model v1.1
4. For each credential in the presentation, makes sure:
a. Makes sure the credential has a valid signature
b. Makes sure the credential is not expired
c. Makes sure the credential complies with the VC Data Model
d. If the credential has a schema, makes sure its data complies with the schema
parameters:
- description: request body
in: body
name: request
required: true
schema:
$ref: '#/definitions/pkg_server_router.VerifyPresentationRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/pkg_server_router.VerifyPresentationResponse'
"400":
description: Bad request
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Verifies a Verifiable Presentation
tags:
- Presentations
/v1/schemas:
get:
consumes:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package credential
package verification

import (
"context"
Expand All @@ -10,26 +10,27 @@ import (
"github.com/TBD54566975/ssi-sdk/crypto"
"github.com/TBD54566975/ssi-sdk/crypto/jwx"
"github.com/TBD54566975/ssi-sdk/cryptosuite/jws2020"
"github.com/TBD54566975/ssi-sdk/did"
"github.com/TBD54566975/ssi-sdk/did/resolution"
sdkutil "github.com/TBD54566975/ssi-sdk/util"
"github.com/goccy/go-json"
"github.com/lestrrat-go/jwx/jws"
"github.com/pkg/errors"

"github.com/tbd54566975/ssi-service/internal/credential"
didint "github.com/tbd54566975/ssi-service/internal/did"
"github.com/tbd54566975/ssi-service/internal/keyaccess"
"github.com/tbd54566975/ssi-service/internal/schema"
)

type Validator struct {
type Verifier struct {
validator *validation.CredentialValidator
didResolver resolution.Resolver
schemaResolver schema.Resolution
}

// NewCredentialValidator creates a new credential validator which executes both signature and static verification checks.
// In the future the set of verification checks will be configurable.
func NewCredentialValidator(didResolver resolution.Resolver, schemaResolver schema.Resolution) (*Validator, error) {
// NewVerifiableDataVerifier creates a new verifier for both verifiable credentials and verifiable presentations. The verifier
// executes both signature and static verification checks. In the future the set of verification checks will be configurable.
func NewVerifiableDataVerifier(didResolver resolution.Resolver, schemaResolver schema.Resolution) (*Verifier, error) {
if didResolver == nil {
return nil, errors.New("didResolver cannot be nil")
}
Expand All @@ -40,30 +41,19 @@ func NewCredentialValidator(didResolver resolution.Resolver, schemaResolver sche
validators := validation.GetKnownVerifiers()
validator, err := validation.NewCredentialValidator(validators)
if err != nil {
return nil, errors.Wrap(err, "failed to create static credential validator")
return nil, errors.Wrap(err, "failed to create static validator")
}
return &Validator{
return &Verifier{
validator: validator,
didResolver: didResolver,
schemaResolver: schemaResolver,
}, nil
}

// 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 Validator) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT) error {
_, err := integrity.VerifyJWTCredential(ctx, token.String(), v.didResolver)
if err != nil {
return errors.Wrap(err, "verifying JWT credential")
}
_, _, cred, err := integrity.ParseVerifiableCredentialFromJWT(token.String())
if err != nil {
return errors.Wrap(err, "parsing vc from jwt")
}
return v.staticValidationChecks(ctx, *cred)
}

func (v Validator) Verify(ctx context.Context, credential Container) error {
// VerifyCredential first parses and checks the signature on the given credential. Next, it runs
// a set of static verification checks on the credential as per the service's configuration.
// Works for both JWT and LD securing mechanisms.
func (v Verifier) VerifyCredential(ctx context.Context, credential credential.Container) error {
if credential.HasJWTCredential() {
err := v.VerifyJWTCredential(ctx, *credential.CredentialJWT)
if err != nil {
Expand All @@ -77,9 +67,23 @@ func (v Validator) Verify(ctx context.Context, credential Container) error {
return nil
}

// 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 Validator) VerifyDataIntegrityCredential(ctx context.Context, credential credsdk.VerifiableCredential) error {
// VerifyJWTCredential first parses and checks the signature on the given JWT verification. Next, it runs
// a set of static verification checks on the credential as per the service's configuration.
func (v Verifier) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT) error {
_, err := integrity.VerifyJWTCredential(ctx, token.String(), v.didResolver)
if err != nil {
return errors.Wrap(err, "verifying JWT credential")
}
_, _, cred, err := integrity.ParseVerifiableCredentialFromJWT(token.String())
if err != nil {
return errors.Wrap(err, "parsing vc from jwt")
}
return v.staticValidationChecks(ctx, *cred)
}

// VerifyDataIntegrityCredential first checks the signature on the given data integrity verification. Next, it runs
// a set of static verification checks on the credential as per the service's configuration.
func (v Verifier) VerifyDataIntegrityCredential(ctx context.Context, credential credsdk.VerifiableCredential) error {
// resolve the issuer's key material
issuer, ok := credential.Issuer.(string)
if !ok {
Expand Down Expand Up @@ -120,56 +124,67 @@ func (v Validator) VerifyDataIntegrityCredential(ctx context.Context, credential
return v.staticValidationChecks(ctx, credential)
}

func getKeyFromProof(proof crypto.Proof, key string) (any, error) {
proofBytes, err := json.Marshal(proof)
// VerifyJWTPresentation first parses and checks the signature on the given JWT presentation. Next, it runs
// a set of static verification checks on the presentation's credentials as per the service's configuration.
func (v Verifier) VerifyJWTPresentation(ctx context.Context, token keyaccess.JWT) error {
headers, jwt, vp, err := integrity.ParseVerifiablePresentationFromJWT(token.String())
if err != nil {
return nil, err
}
var proofMap map[string]any
if err = json.Unmarshal(proofBytes, &proofMap); err != nil {
return nil, err
return errors.Wrap(err, "parsing JWT presentation")
}
return proofMap[key], nil
}

func (v Validator) VerifyJWT(ctx context.Context, did string, token keyaccess.JWT) error {
// parse headers
headers, err := keyaccess.GetJWTHeaders([]byte(token))
if err != nil {
return sdkutil.LoggingErrorMsg(err, "could not parse JWT headers")
// get key to verify the presentation with
issuerKID := headers.KeyID()
if issuerKID == "" {
return errors.Errorf("missing kid in header of presentation<%s>", jwt.JwtID())
}
jwtKID, ok := headers.Get(jws.KeyIDKey)
if !ok {
return sdkutil.LoggingNewError("JWT does not contain a kid")
issuerDID, err := v.didResolver.Resolve(ctx, jwt.Issuer())
if err != nil {
return errors.Wrapf(err, "getting issuer DID<%s> to verify presentation<%s>", jwt.Issuer(), jwt.JwtID())
}
kid, ok := jwtKID.(string)
if !ok {
return sdkutil.LoggingNewError("JWT kid is not a string")
issuerPublicKey, err := did.GetKeyFromVerificationMethod(issuerDID.Document, issuerKID)
if err != nil {
return errors.Wrapf(err, "getting key to verify presentation<%s>", jwt.JwtID())
}

// resolve key material from the DID
pubKey, err := didint.ResolveKeyForDID(ctx, v.didResolver, did, kid)
// construct a verifier and verify the signature on the presentation
// note: this also verifies the signature of each credential in the presentation
verifier, err := jwx.NewJWXVerifier(issuerDID.ID, issuerKID, issuerPublicKey)
if err != nil {
return sdkutil.LoggingError(err)
return errors.Wrapf(err, "constructing verifier for presentation<%s>", jwt.JwtID())
}

// construct a signature validator from the verification information
verifier, err := keyaccess.NewJWKKeyAccessVerifier(did, kid, pubKey)
_, _, _, err = integrity.VerifyVerifiablePresentationJWT(ctx, *verifier, v.didResolver, token.String())
if err != nil {
return sdkutil.LoggingErrorMsgf(err, "could not create validator for kid %s", kid)
return errors.Wrapf(err, "verifying presentation<%s>", jwt.JwtID())
}

// verify the signature on the credential
if err = verifier.Verify(token); err != nil {
return sdkutil.LoggingErrorMsg(err, "could not verify the JWT signature")
// for each credential in the presentation, run a set of static verification checks
creds, err := credential.NewCredentialContainerFromArray(vp.VerifiableCredential)
if err != nil {
return errors.Wrapf(err, "error parsing credentials in presentation<%s>", vp.ID)
}
for _, cred := range creds {
if err = v.staticValidationChecks(ctx, *cred.Credential); err != nil {
return errors.Wrapf(err, "error running static validation checks on credential in presentation<%v>", cred.ID)
}
}

return nil
}

// staticValidationChecks runs a set of static validation checks on the credential as per the credential
// service's configuration, such as checking the credential's schema, expiration, and object validity.
func (v Validator) staticValidationChecks(ctx context.Context, credential credsdk.VerifiableCredential) error {
func getKeyFromProof(proof crypto.Proof, key string) (any, error) {
proofBytes, err := json.Marshal(proof)
if err != nil {
return nil, err
}
var proofMap map[string]any
if err = json.Unmarshal(proofBytes, &proofMap); err != nil {
return nil, err
}
return proofMap[key], nil
}

// staticValidationChecks runs a set of static validation checks on the credential as per the
// service's configuration, such as checking the verification's schema, expiration, and object validity.
func (v Verifier) staticValidationChecks(ctx context.Context, credential credsdk.VerifiableCredential) error {
// if the credential has a schema, resolve it before it is to be used in verification
var validationOpts []validation.Option
if credential.CredentialSchema != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/server/router/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,10 @@ type VerifyCredentialResponse struct {
// VerifyCredential godoc
//
// @Summary Verify a Verifiable Credential
// @Description Verify a given Verifiable Credential by its ID. The system does the following levels of verification:
// @Description Verifies a given verifiable credential. The system does the following levels of verification:
// @Description 1. Makes sure the credential has a valid signature
// @Description 2. Makes sure the credential has is not expired
// @Description 3. Makes sure the credential complies with the VC Data Model
// @Description 3. Makes sure the credential complies with the VC Data Model v1.1
// @Description 4. If the credential has a schema, makes sure its data complies with the schema
// @Tags Credentials
// @Accept json
Expand Down
63 changes: 61 additions & 2 deletions pkg/server/router/presentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import (

"github.com/TBD54566975/ssi-sdk/credential/exchange"
"github.com/TBD54566975/ssi-sdk/credential/integrity"
"github.com/TBD54566975/ssi-sdk/util"
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"github.com/pkg/errors"
"go.einride.tech/aip/filtering"

"github.com/tbd54566975/ssi-service/pkg/server/pagination"

credint "github.com/tbd54566975/ssi-service/internal/credential"
"github.com/tbd54566975/ssi-service/internal/keyaccess"
"github.com/tbd54566975/ssi-service/pkg/server/framework"
"github.com/tbd54566975/ssi-service/pkg/server/pagination"
svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
"github.com/tbd54566975/ssi-service/pkg/service/presentation"
"github.com/tbd54566975/ssi-service/pkg/service/presentation/model"
Expand All @@ -37,6 +37,65 @@ func NewPresentationRouter(s svcframework.Service) (*PresentationRouter, error)
return &PresentationRouter{service: service}, nil
}

type VerifyPresentationRequest struct {
// A JWT that encodes a verifiable presentation according to https://www.w3.org/TR/vc-data-model/#json-web-token
PresentationJWT *keyaccess.JWT `json:"presentationJwt,omitempty" validate:"required"`
}

type VerifyPresentationResponse struct {
// Whether the presentation was verified.
Verified bool `json:"verified"`

// The reason why this presentation couldn't be verified.
Reason string `json:"reason,omitempty"`
}

// VerifyPresentation godoc
//
// @Summary Verifies a Verifiable Presentation
// @Description Verifies a given presentation. The system does the following levels of verification:
// @Description 1. Makes sure the presentation has a valid signature
// @Description 2. Makes sure the presentation is not expired
// @Description 3. Makes sure the presentation complies with https://www.w3.org/TR/vc-data-model/#presentations-0 of VC Data Model v1.1
// @Description 4. For each credential in the presentation, makes sure:
// @Description a. Makes sure the credential has a valid signature
// @Description b. Makes sure the credential is not expired
// @Description c. Makes sure the credential complies with the VC Data Model
// @Description d. If the credential has a schema, makes sure its data complies with the schema
// @Tags Presentations
// @Accept json
// @Produce json
// @Param request body VerifyPresentationRequest true "request body"
// @Success 200 {object} VerifyPresentationResponse
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /v1/presentations/verification [put]
func (pr PresentationRouter) VerifyPresentation(c *gin.Context) {
var request VerifyPresentationRequest
if err := framework.Decode(c.Request, &request); err != nil {
errMsg := "invalid verify presentation request"
framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusBadRequest)
return
}

if err := util.IsValidStruct(request); err != nil {
framework.LoggingRespondError(c, err, http.StatusBadRequest)
return
}

verificationResult, err := pr.service.VerifyPresentation(c, presentation.VerifyPresentationRequest{
PresentationJWT: request.PresentationJWT,
})
if err != nil {
errMsg := "could not verify presentation"
framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
return
}

resp := VerifyPresentationResponse{Verified: verificationResult.Verified, Reason: verificationResult.Reason}
framework.Respond(c, resp, http.StatusOK)
}

type CreatePresentationDefinitionRequest struct {
Name string `json:"name,omitempty"`
Purpose string `json:"purpose,omitempty"`
Expand Down
Loading