Repository: TBD54566975/ssi-service
Pull request #636 (2023-08-03 22:30:22)

Documentation for Verifying Presentations

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/636?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#636](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/636?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (ecc63e8) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/f1b0954086a9e5fa61a9b86e6df69b7bcbea3467?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (f1b0954) will **decrease** coverage by `1.16%`.
> Report is 1 commits behind head on main.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #636      +/-   ##
==========================================
- Coverage   26.54%   25.39%   -1.16%     
==========================================
  Files          55       56       +1     
  Lines        5876     6222     +346     
==========================================
+ Hits         1560     1580      +20     
- Misses       4044     4367     +323     
- Partials      272      275       +3     
```


| [Files Changed](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/636?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/server.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/636?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9zZXJ2ZXIuZ28=) | `71.25% <100.00%> (+0.45%)` | :arrow_up: |

... and [5 files with indirect coverage changes](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/636/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)



Patch:
diff --git a/doc/howto/presentation.md b/doc/howto/presentation.md
new file mode 100644
index 0000000..b2c11c8
--- /dev/null
+++ b/doc/howto/presentation.md
@@ -0,0 +1,104 @@
+# How To: Verify a Presentation
+
+## Background
+
+By now you should be familiar with [DIDs](did.md) and [Verifiable Credentials](credential.md). A next logical question, once you've started using the technologies, is "how do I make use of it?" To use Verifiable Credentials anywhere you need to present them — that is to share them with another party. There are a few pieces of sharing: creating a package that you share and transmitting the data to another party. Transmission, simliar to credential issuance, can be accomplished with a number of different mechanisms like [Web5](https://github.com/TBD54566975/dwn-sdk-js#readme) and [OpenID Connect](https://openid.net/sg/openid4vc/). Packaging a credential, or set of credentials, into a presentation is something that has been standardized at the W3C in the same specification that defines Verifiable Credentials, called [Verifiable Presentations](https://www.w3.org/TR/vc-data-model/#presentations-0).
+
+Verifiable Presentations are a standard data container for sharing credentials that provide a number of benefits, namely:
+
+1. A common data format designed to work with Verifiable Credentials, reusable in any number of use cases.
+2. Guidance on constructing presentations that ensures integrity protection and guards against replay attacks.
+
+Presentations impose no constraints on who can construct a presentation, or what may be presented. This means if you have multiple credentials issued to different DIDs, you can still construct a Verifiable Presentation to present those credentials at the same time. This is why you will see that the [`proof` property](https://www.w3.org/TR/vc-data-model/#example-basic-structure-of-a-presentation) can be an array. Verifiers must take care to make sure that the credentials in a given presentation _can be_ presented by the presenter, and that the credentials themselves are valid.
+
+Verifiable Presentations are currently accepted in two main SSI Service flows: when verifying [Presentation Submissoins](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) while using [Presentation Exchange](https://identity.foundation/presentation-exchange/spec/v2.0.0), and verifying [Credential Applications](https://identity.foundation/credential-manifest/#credential-application) while using [Credential Manifest](https://identity.foundation/credential-manifest). As a utility, we've also exposed an endpoint to statelessly verify a presentation at `/v1/presentations/verification`.
+
+## Constructing a Verifiable Presentation
+
+Constructing a Verifiable Presentation is out of scope for the SSI Service, since the service acts as a utility for organizations managing their own credentials. However, the [SSI SDK](https://github.com/TBD54566975/ssi-sdk) provides a standards-based implementation of Verifiable Presentations using the [JWT representation](https://www.w3.org/TR/vc-data-model/#jwt-and-jws-considerations).
+
+Library code for Verifiable Presentations can be found [here](https://github.com/TBD54566975/ssi-sdk/blob/d5c302a1d9b9d04c1636a0c8dfda015f61bb0f6b/credential/model.go#L110) with associated signing and verificatoin logic [here](https://github.com/TBD54566975/ssi-sdk/blob/d5c302a1d9b9d04c1636a0c8dfda015f61bb0f6b/credential/integrity/jwt.go#L208). An example of constructing a Verifiable Presentation using the SDK is provided below as a [runnable unit test here](presentation_test.go).
+
+Upon running, we should see the test create a credential and a presentation for that credential as a JWT.
+
+```plaintext
+eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrI3o2TWttTjEyOTZ1YXBIbU02QTI4bkdaR2RBRW5pRDFhYTVSZEZDbjhKRXVucVY5ayIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTExNzU2MjQsImlzcyI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrIiwianRpIjoiYjQ0OTI0ZWEtMDIwMi00ZTllLWJiMmEtOTg5YmQwNWQ1N2FlIiwibmJmIjoxNjkxMTc1NjI0LCJub25jZSI6IjVjNmFhZDc2LWUyZWYtNGNiNy1iMWE4LTI3MjZiMjRhM2Y0ZSIsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSkZaRVJUUVNJc0ltdHBaQ0k2SW1ScFpEcHJaWGs2ZWpaTmEyMU9NVEk1Tm5WaGNFaHRUVFpCTWpodVIxcEhaRUZGYm1sRU1XRmhOVkprUmtOdU9FcEZkVzV4VmpsckkzbzJUV3R0VGpFeU9UWjFZWEJJYlUwMlFUSTRia2RhUjJSQlJXNXBSREZoWVRWU1pFWkRiamhLUlhWdWNWWTVheUlzSW5SNWNDSTZJa3BYVkNKOS5leUpwWVhRaU9qRTJPVEV4TnpVMk1qUXNJbWx6Y3lJNkltUnBaRHByWlhrNmVqWk5hMjFPTVRJNU5uVmhjRWh0VFRaQk1qaHVSMXBIWkVGRmJtbEVNV0ZoTlZKa1JrTnVPRXBGZFc1eFZqbHJJaXdpYW5ScElqb2lPRFUyTnpRellURXRZamxsWWkwME56Z3lMV0kzTkRjdE5UbGtOekkzWVRGaVlXWTFJaXdpYm1KbUlqb3hOamt4TVRjMU5qSTBMQ0p1YjI1alpTSTZJamsyTkRkalltTmtMV0k0WkRndE5HVXhNeTFpTURKa0xURXpZelUxTm1RNVlqRTFPQ0lzSW5OMVlpSTZJbVJwWkRwclpYazZlalpOYTIxT01USTVOblZoY0VodFRUWkJNamh1UjFwSFpFRkZibWxFTVdGaE5WSmtSa051T0VwRmRXNXhWamxySWl3aWRtTWlPbnNpUUdOdmJuUmxlSFFpT2xzaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlYU3dpZEhsd1pTSTZXeUpXWlhKcFptbGhZbXhsUTNKbFpHVnVkR2xoYkNKZExDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDSTZleUpsYlhCc2IzbGxjaUk2SWxSQ1JDSXNJbXB2WWxScGRHeGxJam9pVkhWMGIzSnBZV3dnUVhWMGFHOXlJbjE5ZlEuWEJPbjBTd2RZZUMwN2dHM1VkT1ZLeHV2YXpfWVRpRkNmZ2tpZXJhZUZnVkEtT2tkWDM1SWl6T0NhdUtqdWlsQXJzZklvMkNYN1pYaDl3djRhUXZFRGciXX19.pJXQXSJcu4U752IE0IH21Yw26OsGMLrHE_-LpGLDHkfetQoJk56j9Fflg-P68xVgfNwZ4EBgGEJ88bXLRv1aDQ
+```
+
+Upon decoding, we can view the JWT as:
+
+```json
+{
+  "alg": "EdDSA",
+  "kid": "did:key:z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k#z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k",
+  "typ": "JWT"
+}
+```
+
+```json
+{
+  "iat": 1691175624,
+  "iss": "did:key:z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k",
+  "jti": "b44924ea-0202-4e9e-bb2a-989bd05d57ae",
+  "nbf": 1691175624,
+  "nonce": "5c6aad76-e2ef-4cb7-b1a8-2726b24a3f4e",
+  "vp": {
+    "@context": ["https://www.w3.org/2018/credentials/v1"],
+    "type": ["VerifiablePresentation"],
+    "verifiableCredential": [
+      "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrI3o2TWttTjEyOTZ1YXBIbU02QTI4bkdaR2RBRW5pRDFhYTVSZEZDbjhKRXVucVY5ayIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTExNzU2MjQsImlzcyI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrIiwianRpIjoiODU2NzQzYTEtYjllYi00NzgyLWI3NDctNTlkNzI3YTFiYWY1IiwibmJmIjoxNjkxMTc1NjI0LCJub25jZSI6Ijk2NDdjYmNkLWI4ZDgtNGUxMy1iMDJkLTEzYzU1NmQ5YjE1OCIsInN1YiI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJlbXBsb3llciI6IlRCRCIsImpvYlRpdGxlIjoiVHV0b3JpYWwgQXV0aG9yIn19fQ.XBOn0SwdYeC07gG3UdOVKxuvaz_YTiFCfgkieraeFgVA-OkdX35IizOCauKjuilArsfIo2CX7ZXh9wv4aQvEDg"
+    ]
+  }
+}
+```
+
+We can also decode the JWT VC value within the `verifiableCredential` property (the one that ends with `v4aQvEDg`) as:
+
+```json
+{
+  "alg": "EdDSA",
+  "kid": "did:key:z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k#z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k",
+  "typ": "JWT"
+}
+```
+
+```json
+{
+  "iat": 1691175624,
+  "iss": "did:key:z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k",
+  "jti": "856743a1-b9eb-4782-b747-59d727a1baf5",
+  "nbf": 1691175624,
+  "nonce": "9647cbcd-b8d8-4e13-b02d-13c556d9b158",
+  "sub": "did:key:z6MkmN1296uapHmM6A28nGZGdAEniD1aa5RdFCn8JEunqV9k",
+  "vc": {
+    "@context": [
+      "https://www.w3.org/2018/credentials/v1"
+    ],
+    "type": [
+      "VerifiableCredential"
+    ],
+    "credentialSubject": {
+      "employer": "TBD",
+      "jobTitle": "Tutorial Author"
+    }
+  }
+}
+```
+
+## Verifying a Presentation
+
+Once we have a Verifiable Presentation we can use the service to verify it by making a `PUT` request to `/v1/presentations/verification`. A sample request is as follows:
+
+```bash
+curl -X PUT localhost:3000/v1/presentations/verification -d '{
+    "presentationJwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrI3o2TWttTjEyOTZ1YXBIbU02QTI4bkdaR2RBRW5pRDFhYTVSZEZDbjhKRXVucVY5ayIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTExNzU2MjQsImlzcyI6ImRpZDprZXk6ejZNa21OMTI5NnVhcEhtTTZBMjhuR1pHZEFFbmlEMWFhNVJkRkNuOEpFdW5xVjlrIiwianRpIjoiYjQ0OTI0ZWEtMDIwMi00ZTllLWJiMmEtOTg5YmQwNWQ1N2FlIiwibmJmIjoxNjkxMTc1NjI0LCJub25jZSI6IjVjNmFhZDc2LWUyZWYtNGNiNy1iMWE4LTI3MjZiMjRhM2Y0ZSIsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSkZaRVJUUVNJc0ltdHBaQ0k2SW1ScFpEcHJaWGs2ZWpaTmEyMU9NVEk1Tm5WaGNFaHRUVFpCTWpodVIxcEhaRUZGYm1sRU1XRmhOVkprUmtOdU9FcEZkVzV4VmpsckkzbzJUV3R0VGpFeU9UWjFZWEJJYlUwMlFUSTRia2RhUjJSQlJXNXBSREZoWVRWU1pFWkRiamhLUlhWdWNWWTVheUlzSW5SNWNDSTZJa3BYVkNKOS5leUpwWVhRaU9qRTJPVEV4TnpVMk1qUXNJbWx6Y3lJNkltUnBaRHByWlhrNmVqWk5hMjFPTVRJNU5uVmhjRWh0VFRaQk1qaHVSMXBIWkVGRmJtbEVNV0ZoTlZKa1JrTnVPRXBGZFc1eFZqbHJJaXdpYW5ScElqb2lPRFUyTnpRellURXRZamxsWWkwME56Z3lMV0kzTkRjdE5UbGtOekkzWVRGaVlXWTFJaXdpYm1KbUlqb3hOamt4TVRjMU5qSTBMQ0p1YjI1alpTSTZJamsyTkRkalltTmtMV0k0WkRndE5HVXhNeTFpTURKa0xURXpZelUxTm1RNVlqRTFPQ0lzSW5OMVlpSTZJbVJwWkRwclpYazZlalpOYTIxT01USTVOblZoY0VodFRUWkJNamh1UjFwSFpFRkZibWxFTVdGaE5WSmtSa051T0VwRmRXNXhWamxySWl3aWRtTWlPbnNpUUdOdmJuUmxlSFFpT2xzaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlYU3dpZEhsd1pTSTZXeUpXWlhKcFptbGhZbXhsUTNKbFpHVnVkR2xoYkNKZExDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDSTZleUpsYlhCc2IzbGxjaUk2SWxSQ1JDSXNJbXB2WWxScGRHeGxJam9pVkhWMGIzSnBZV3dnUVhWMGFHOXlJbjE5ZlEuWEJPbjBTd2RZZUMwN2dHM1VkT1ZLeHV2YXpfWVRpRkNmZ2tpZXJhZUZnVkEtT2tkWDM1SWl6T0NhdUtqdWlsQXJzZklvMkNYN1pYaDl3djRhUXZFRGciXX19.pJXQXSJcu4U752IE0IH21Yw26OsGMLrHE_-LpGLDHkfetQoJk56j9Fflg-P68xVgfNwZ4EBgGEJ88bXLRv1aDQ"
+}'
+```
+
+Upon success we see a response such as:
+
+```json
+{ 
+  "verified": true
+}
+```
diff --git a/doc/howto/verification.md b/doc/howto/verification.md
index 92e98f7..f465b04 100644
--- a/doc/howto/verification.md
+++ b/doc/howto/verification.md
@@ -53,7 +53,7 @@ Upon success we see a response such as:
 
 ### Verifiable Presentations
 
-The example we've gone through above verifies a credential from an _issuer_. But what about verifying the _presentation_ of a credential, or set of credentials, from a _holder_ to a _verifier_? To do this, a holder must construct what's called a [Verifiable Presentation](https://www.w3.org/TR/vc-data-model/#presentations-0), an object which is also defined by the VC Data Model, which allows a _holder_ of a verifiable credential to create an authenticated wrapper around a set of credentials it wishes to present to a _verifier_. The service's verification API does [not yet](https://github.com/TBD54566975/ssi-service/issues/615) support this functionality.
+The example we've gone through above verifies a credential from an _issuer_. But what about verifying the _presentation_ of a credential, or set of credentials, from a _holder_ to a _verifier_? To do this, a holder must construct what's called a [Verifiable Presentation](https://www.w3.org/TR/vc-data-model/#presentations-0), an object which is also defined by the VC Data Model, which allows a _holder_ of a verifiable credential to create an authenticated wrapper around a set of credentials it wishes to present to a _verifier_. Learn more in [our guide on presentations here](presentation.md).
 
 ### Presentation Exchange
 
diff --git a/go.mod b/go.mod
index c4ac315..c121581 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.20
 
 require (
 	github.com/BurntSushi/toml v1.3.2
-	github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230731175253-d5c302a1d9b9
+	github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230804184541-a62922c45264
 	github.com/alicebob/miniredis/v2 v2.30.4
 	github.com/ardanlabs/conf v1.5.0
 	github.com/benbjohnson/clock v1.3.5
diff --git a/go.sum b/go.sum
index 6488a61..86c37dc 100644
--- a/go.sum
+++ b/go.sum
@@ -48,6 +48,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
 github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
 github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230731175253-d5c302a1d9b9 h1:Ig2o+eOTFTaa9agWiz+Vz/7N4zoTJ2Na9PiRaYaAbXY=
 github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230731175253-d5c302a1d9b9/go.mod h1:mVKRjfdpgmCxPwnfQluXGkgzsFyrPsjrCvHXCJ41avQ=
+github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230804184541-a62922c45264 h1:5Sjk7Q8M9TNYWc3DV4SDORRxdNbFINz8JHkHCGeAnn4=
+github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230804184541-a62922c45264/go.mod h1:mVKRjfdpgmCxPwnfQluXGkgzsFyrPsjrCvHXCJ41avQ=
 github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
 github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
 github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
diff --git a/internal/keyaccess/jwt.go b/internal/keyaccess/jwt.go
index 689add4..ed42b0d 100644
--- a/internal/keyaccess/jwt.go
+++ b/internal/keyaccess/jwt.go
@@ -145,7 +145,7 @@ func (ka JWKKeyAccess) SignVerifiablePresentation(audience string, presentation
 	if err := presentation.IsValid(); err != nil {
 		return nil, errors.New("cannot sign invalid presentation")
 	}
-	tokenBytes, err := integrity.SignVerifiablePresentationJWT(*ka.Signer, integrity.JWTVVPParameters{Audience: []string{audience}}, presentation)
+	tokenBytes, err := integrity.SignVerifiablePresentationJWT(*ka.Signer, &integrity.JWTVVPParameters{Audience: []string{audience}}, presentation)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not sign presentation")
 	}
diff --git a/internal/verification/verification.go b/internal/verification/verification.go
index 20848e2..baf5bef 100644
--- a/internal/verification/verification.go
+++ b/internal/verification/verification.go
@@ -10,7 +10,6 @@ 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"
@@ -127,40 +126,18 @@ func (v Verifier) VerifyDataIntegrityCredential(ctx context.Context, credential
 // 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())
+	_, err := integrity.VerifyJWTPresentation(ctx, token.String(), v.didResolver)
 	if err != nil {
-		return errors.Wrap(err, "parsing JWT presentation")
+		return errors.Wrap(err, "verifying JWT presentation")
 	}
-
-	// get key to verify the presentation with
-	issuerKID := headers.KeyID()
-	if issuerKID == "" {
-		return errors.Errorf("missing kid in header of presentation<%s>", jwt.JwtID())
-	}
-	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())
-	}
-	issuerPublicKey, err := did.GetKeyFromVerificationMethod(issuerDID.Document, issuerKID)
+	_, _, pres, err := integrity.ParseVerifiablePresentationFromJWT(token.String())
 	if err != nil {
-		return errors.Wrapf(err, "getting key to verify presentation<%s>", jwt.JwtID())
-	}
-
-	// 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 errors.Wrapf(err, "constructing verifier for presentation<%s>", jwt.JwtID())
-	}
-	_, _, _, err = integrity.VerifyVerifiablePresentationJWT(ctx, *verifier, v.didResolver, token.String())
-	if err != nil {
-		return errors.Wrapf(err, "verifying presentation<%s>", jwt.JwtID())
+		return errors.Wrap(err, "parsing vc from jwt")
 	}
-
 	// for each credential in the presentation, run a set of static verification checks
-	creds, err := credential.NewCredentialContainerFromArray(vp.VerifiableCredential)
+	creds, err := credential.NewCredentialContainerFromArray(pres.VerifiableCredential)
 	if err != nil {
-		return errors.Wrapf(err, "error parsing credentials in presentation<%s>", vp.ID)
+		return errors.Wrapf(err, "error parsing credentials in presentation<%s>", pres.ID)
 	}
 	for _, cred := range creds {
 		if err = v.staticValidationChecks(ctx, *cred.Credential); err != nil {
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 6e3bb28..9a986b8 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -232,6 +232,9 @@ func PresentationAPI(rg *gin.RouterGroup, service svcframework.Service, webhookS
 	// make sure the presentation service is configured to use the correct path
 	config.SetServicePath(svcframework.Presentation, PresentationsPrefix)
 
+	presAPI := rg.Group(PresentationsPrefix)
+	presAPI.PUT(VerificationPath, presRouter.VerifyPresentation)
+
 	presDefAPI := rg.Group(PresentationsPrefix + DefinitionsPrefix)
 	presDefAPI.PUT("", presRouter.CreateDefinition)
 	presDefAPI.GET("/:id", presRouter.GetDefinition)


Test patch:
diff --git a/doc/howto/presentation_test.go b/doc/howto/presentation_test.go
new file mode 100644
index 0000000..f904329
--- /dev/null
+++ b/doc/howto/presentation_test.go
@@ -0,0 +1,78 @@
+package howto
+
+import (
+	"testing"
+	"time"
+
+	"github.com/TBD54566975/ssi-sdk/credential"
+	"github.com/TBD54566975/ssi-sdk/credential/integrity"
+	"github.com/TBD54566975/ssi-sdk/crypto"
+	"github.com/TBD54566975/ssi-sdk/crypto/jwx"
+	"github.com/TBD54566975/ssi-sdk/did/key"
+	"github.com/stretchr/testify/require"
+)
+
+func TestVerifiablePresentation(t *testing.T) {
+	// create a new DID to use to self-issue a credential and present it to a verifier
+	privateKey, didKey, err := key.GenerateDIDKey(crypto.Ed25519)
+	require.NoError(t, err)
+
+	// expand the DID Document to access the key ID
+	didKeyDocument, err := didKey.Expand()
+	require.NoError(t, err)
+	didKeyID := didKeyDocument.VerificationMethod[0].ID
+
+	// first create a credential we wish to present
+	credBuilder := credential.NewVerifiableCredentialBuilder()
+
+	// set our DID as the issuer of the credential, since it will be self-signed
+	err = credBuilder.SetIssuer(didKey.String())
+	require.NoError(t, err)
+
+	// set the issuance date as the current time
+	err = credBuilder.SetIssuanceDate(time.Now().Format(time.RFC3339))
+	require.NoError(t, err)
+
+	// add the data to the body of the credential
+	err = credBuilder.SetCredentialSubject(
+		map[string]any{
+			// set the ID property of the credential subject to our DID, since the credential is about us
+			credential.VerifiableCredentialIDProperty: didKey.String(),
+			"jobTitle": "Tutorial Author",
+			"employer": "TBD",
+		},
+	)
+	require.NoError(t, err)
+
+	// build the credential
+	unsignedCredential, err := credBuilder.Build()
+	require.NoError(t, err)
+
+	// sign the credential using our DID's private key
+	signer, err := jwx.NewJWXSigner(didKey.String(), didKeyID, privateKey)
+	require.NoError(t, err)
+
+	// sign as a JWT
+	signedCredentialBytes, err := integrity.SignVerifiableCredentialJWT(*signer, *unsignedCredential)
+	require.NoError(t, err)
+	signedCredentialJWTString := string(signedCredentialBytes)
+
+	// construct a Verifiable Presentation for the credential
+	presBuilder := credential.NewVerifiablePresentationBuilder()
+	err = presBuilder.SetHolder(didKey.String())
+	require.NoError(t, err)
+	err = presBuilder.AddVerifiableCredentials(signedCredentialJWTString)
+	require.NoError(t, err)
+
+	// build the presentation
+	unsignedPresentation, err := presBuilder.Build()
+	require.NoError(t, err)
+
+	// sign the presentation using our DID's private key, passing no additional parameters
+	signedPresentationBytes, err := integrity.SignVerifiablePresentationJWT(*signer, nil, *unsignedPresentation)
+	require.NoError(t, err)
+	signedPresentationJWTString := string(signedPresentationBytes)
+
+	// print the signed presentation JWT
+	println(signedPresentationJWTString)
+}
diff --git a/internal/keyaccess/jwt_test.go b/internal/keyaccess/jwt_test.go
index 1388525..7dfd121 100644
--- a/internal/keyaccess/jwt_test.go
+++ b/internal/keyaccess/jwt_test.go
@@ -328,7 +328,7 @@ func getJWTTestPresentation(ka JWKKeyAccess) credential.VerifiablePresentation {
 	knownContext := []string{"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"}
 	knownID := uuid.NewString()
 	knownType := []string{"VerifiablePresentation", "HappyPresentation"}
-	knownHolder := "did:example:ebfeb1f712ebc6f1c276e12ec21"
+	knownHolder := ka.Signer.ID
 	testCredential := getTestCredential(ka.Signer.ID)
 	signedJWT, _ := ka.SignVerifiableCredential(testCredential)
 	return credential.VerifiablePresentation{
diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go
index 93c4a53..93438b0 100644
--- a/pkg/server/server_presentation_test.go
+++ b/pkg/server/server_presentation_test.go
@@ -110,7 +110,7 @@ func TestPresentationAPI(t *testing.T) {
 
 				ttt.Run("Invalid Verifiable Presentation with no credentials", func(tttt *testing.T) {
 					// use the sdk to create a vp
-					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
+					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, &integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
 					assert.NoError(tttt, err)
 
 					badPresentation := string(emptyPresentation[:10])
@@ -124,12 +124,12 @@ func TestPresentationAPI(t *testing.T) {
 					var resp router.VerifyPresentationResponse
 					assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp))
 					assert.False(tttt, resp.Verified)
-					assert.Equal(tttt, resp.Reason, "parsing JWT presentation: parsing vp token: invalid JWT")
+					assert.Equal(tttt, "verifying JWT presentation: parsing JWT: parsing vp token: invalid JWT", resp.Reason)
 				})
 
 				ttt.Run("Valid Verifiable Presentation with no credentials", func(tttt *testing.T) {
 					// use the sdk to create a vp
-					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
+					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, &integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
 					assert.NoError(tttt, err)
 
 					value := newRequestValue(t, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(string(emptyPresentation))})
@@ -150,7 +150,7 @@ func TestPresentationAPI(t *testing.T) {
 					testPresentation.VerifiableCredential = []any{badCredJWT}
 
 					// use the sdk to create a vp
-					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
+					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, &integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
 					assert.NoError(tt, err)
 
 					value := newRequestValue(tttt, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(string(emptyPresentation))})
@@ -171,7 +171,7 @@ func TestPresentationAPI(t *testing.T) {
 					testPresentation.VerifiableCredential = []any{createResp.CredentialJWT}
 
 					// use the sdk to create a vp
-					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
+					emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, &integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation)
 					assert.NoError(tttt, err)
 
 					value := newRequestValue(tttt, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(string(emptyPresentation))})
@@ -947,7 +947,7 @@ func createSubmissionRequest(t *testing.T, definitionID, requesterDID string, vc
 		VerifiableCredential:   []any{keyaccess.JWT(vcData)},
 	}
 
-	signed, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{requesterDID}}, vp)
+	signed, err := integrity.SignVerifiablePresentationJWT(holderSigner, &integrity.JWTVVPParameters{Audience: []string{requesterDID}}, vp)
 	require.NoError(t, err)
 
 	request := router.CreateSubmissionRequest{SubmissionJWT: keyaccess.JWT(signed)}