Repository: sigstore/rekor
Pull request #973 (2022-08-15 15:26:48)

Intoto v0.0.2
<!--
Thanks for opening a pull request! Please do not just delete this text.  The three fields below are mandatory.

Please remember to:
- This PR requires an issue. If it is a new feature, the issue should proceed the PR and will have allowed sufficent time for discussions to take place. Pleases use
issue tags such as "Closes #XYZ" or "Resolves sigstore/repo-name#XYZ".
  [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
- ensure your commits are signed-off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits
- lastly, ensure there are no merge commits!

Thank you :)
-->

#### Summary
<!--
 Explain the **motivation** for making this change. What existing problem does the pull request solve? How can reviewers test this PR?
-->

The current implementation of the intoto type within Rekor does not persist the signatures from the wrapping DSSE envelope into the log entry stored by Trillian. This makes it impossible to independently verify the cryptographic validity of the entry without possession of the original DSSE envelope.

Based on the discussion and design doc, it was decided to create a v0.0.2 of the intoto Rekor type with changes from v0.0.1. In addition also persisting multiple public keys and signatures. 

Design Doc - https://docs.google.com/document/d/17gB598uEkoxx8j9sDhrvfhuNFHW5BSsWEiMP8POn8xo/edit?resourcekey=0-1H4eG4-4-UQYIEXZgj6AKQ#heading=h.6dq5va2kfzsw

Fixes https://github.com/sigstore/rekor/issues/582

#### Release Note
<!--
Add a release note for each of the following conditions:

* Config changes (additions, deletions, updates)
* API additions—new endpoint, new response fields, or newly accepted request parameters
* Database changes (any)
* Websocket additions or changes
* Anything noteworthy to an administrator running private sigstore instances (err on the side of over-communicating)
* New features and improvements, including behavioural changes, UI changes and CLI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes

If no release notes are required write NONE. Use past-tense.

-->

Adds new v0.0.2 for intoto type into rekor for support of DSSE envelope with multi signature and public key

#### Documentation
<!--

Does this change require an update to documentation? How will users implement your new feature?

Please reference a PR within https://docs.sigstore.dev

-->

pxp928: @bobcallaway Ready for another review.
codecov-commenter: # [Codecov](https://codecov.io/gh/sigstore/rekor/pull/973?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) Report
> Merging [#973](https://codecov.io/gh/sigstore/rekor/pull/973?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) (fd253b5) into [main](https://codecov.io/gh/sigstore/rekor/commit/828f6894f44aa686d115e844935097a718f86736?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) (828f689) will **increase** coverage by `0.11%`.
> The diff coverage is `47.85%`.

> :exclamation: Current head fd253b5 differs from pull request most recent head d855eea. Consider uploading reports for the commit d855eea to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #973      +/-   ##
==========================================
+ Coverage   48.06%   48.17%   +0.11%     
==========================================
  Files          61       62       +1     
  Lines        5413     5741     +328     
==========================================
+ Hits         2602     2766     +164     
- Misses       2528     2676     +148     
- Partials      283      299      +16     
```


| [Impacted Files](https://codecov.io/gh/sigstore/rekor/pull/973?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) | Coverage Δ | |
|---|---|---|
| [cmd/rekor-cli/app/root.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-Y21kL3Jla29yLWNsaS9hcHAvcm9vdC5nbw==) | `20.68% <ø> (ø)` | |
| [pkg/types/entries.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL2VudHJpZXMuZ28=) | `6.25% <ø> (ø)` | |
| [pkg/types/intoto/intoto.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL2ludG90by9pbnRvdG8uZ28=) | `37.03% <0.00%> (ø)` | |
| [pkg/types/intoto/v0.0.2/entry.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL2ludG90by92MC4wLjIvZW50cnkuZ28=) | `43.75% <43.75%> (ø)` | |
| [cmd/rekor-cli/app/pflag\_groups.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-Y21kL3Jla29yLWNsaS9hcHAvcGZsYWdfZ3JvdXBzLmdv) | `85.91% <76.47%> (-1.29%)` | :arrow_down: |
| [cmd/rekor-cli/app/pflags.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-Y21kL3Jla29yLWNsaS9hcHAvcGZsYWdzLmdv) | `85.29% <85.00%> (-0.04%)` | :arrow_down: |
| [pkg/sharding/ranges.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3NoYXJkaW5nL3Jhbmdlcy5nbw==) | `46.01% <0.00%> (-1.26%)` | :arrow_down: |
| [pkg/types/rekord/v0.0.1/entry.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL3Jla29yZC92MC4wLjEvZW50cnkuZ28=) | `49.00% <0.00%> (+0.66%)` | :arrow_up: |
| [pkg/types/alpine/v0.0.1/entry.go](https://codecov.io/gh/sigstore/rekor/pull/973/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL2FscGluZS92MC4wLjEvZW50cnkuZ28=) | `56.01% <0.00%> (+2.48%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore)

dlorenc: Looks like something is up with codegen here....
bobcallaway: looks like some of the path names in `Makefile.swagger` have an extra `/` in them for some reason.
pxp928: Looks like the MakeFile is doing unwanted substitution?
```
@echo "SWAGGER_GEN=`find pkg/generated/client pkg/generated/models/ pkg/generated/restapi/ -iname '*.go' | grep -v 'configure_rekor_server' | sort -d | tr '\n' ' ' | sed 's/ $$//'`" >> Makefile.swagger;
```
bobcallaway: > Looks like the Makefile is doing unwanted substitution?
> 
> ```
> @echo "SWAGGER_GEN=`find pkg/generated/client pkg/generated/models/ pkg/generated/restapi/ -iname '*.go' | grep -v 'configure_rekor_server' | sort -d | tr '\n' ' ' | sed 's/ $$//'`" >> Makefile.swagger;
> ```

https://github.com/sigstore/rekor/pull/984 pushed to fix this
dlorenc: Tests seem stuck here. 
dlorenc: Github is back, you might need to force push/rebase to kick the tests though.
pxp928: Fixing up the tests.
pxp928: Any thoughts on why the e2e test fail on `[GET /api/v1/log/entries/{entryUUID}][404] getLogEntryByUuidNotFound`. The entry seems to get created successfully.
priyawadhwa: > Any thoughts on why the e2e test fail on [GET /api/v1/log/entries/{entryUUID}][404] getLogEntryByUuidNotFound. The entry seems to get created successfully.

I ran it locally and got this error:

```
    e2e_test.go:527: [POST /api/v1/log/entries][400] createLogEntryBadRequest  &{Code:400 Message:Error processing entry: intoto implementation for version '0.0.1' not found: unable to locate entry for version 0.0.1}
```


probably related to this comment https://github.com/sigstore/rekor/pull/973/files#r948283591

pxp928: > > Any thoughts on why the e2e test fail on [GET /api/v1/log/entries/{entryUUID}][404] getLogEntryByUuidNotFound. The entry seems to get created successfully.
> 
> I ran it locally and got this error:
> 
> ```
>     e2e_test.go:527: [POST /api/v1/log/entries][400] createLogEntryBadRequest  &{Code:400 Message:Error processing entry: intoto implementation for version '0.0.1' not found: unable to locate entry for version 0.0.1}
> ```
> 
> probably related to this comment https://github.com/sigstore/rekor/pull/973/files#r948283591

Still getting the same error on the e2e test: `[GET /api/v1/log/entries/{entryUUID}][404] getLogEntryByUuidNotFound`. Did some testing and it looks like the entry does get created. When I run it twice, it does say an entry is present already.

pxp928: > Still getting the same error on the e2e test: [GET /api/v1/log/entries/{entryUUID}][404] getLogEntryByUuidNotFound. Did some testing and it looks like the entry does get created. When I run it twice, it does say an entry is present already.

Any thoughts on why this error is occurring? I double checked everything multiple times and nothing apparent.
bobcallaway: > > Still getting the same error on the e2e test: [GET /api/v1/log/entries/{entryUUID}][404] getLogEntryByUuidNotFound. Did some testing and it looks like the entry does get created. When I run it twice, it does say an entry is present already.
> 
> Any thoughts on why this error is occurring? I double checked everything multiple times and nothing apparent.

Off of a quick debug, I believe it is because you have `content.Envelope.Payload` as required. It will be present upon upload but since it is not persisted into the log you have errors reading it back out. I think you have to set it as optional and deal with the conditional in `Validate()` (see v0.0.1 for the corollary)

priyawadhwa: I was able to repro this locally and keep getting this error when doing `rekor-cli get` with either logIndex or uuid:

```
=== RUN   TestIntoto
    e2e_test.go:527: [../rekor-cli upload --artifact /var/folders/fz/xhw4wqps4d165vcmnt_dhy040000gn/T/TestIntoto32180235/001/attestation.json --type intoto --public-key /var/folders/fz/xhw4wqps4d165vcmnt_dhy040000gn/T/TestIntoto32180235/001/pub.pem --rekor_server=http://localhost:3000]
        Created entry at index 0, available at: http://localhost:3000/api/v1/log/entries/66e8fc5ce39f63dbc3a7dbc9fe0188d7c050c89216235416360a834a6d5442e4
        
    e2e_test.go:531: Got uuid:  66e8fc5ce39f63dbc3a7dbc9fe0188d7c050c89216235416360a834a6d5442e4
    e2e_test.go:533: [../rekor-cli get --uuid 66e8fc5ce39f63dbc3a7dbc9fe0188d7c050c89216235416360a834a6d5442e4 --format=json --rekor_server=http://localhost:3000]
        validation failure list:
        validation failure list:
        validation failure list:
        content.content.envelope.content.envelope.payload in body is required
        
    e2e_test.go:533: exit status 1
--- FAIL: TestIntoto (0.31s)
```

Looks like `rekor-cli upload` is successful, but when `rekor-cli get` is run it fails on verification. Seems like some fields are expected but not present? The error is coming from the call to [IntotoObj.Validate](https://github.com/sigstore/rekor/pull/973/files#diff-5baa5429646882e5392613a47233048ad95214ed1c23e4caee9cd9d692f4e254R168). Maybe the attestation that is being uploaded needs to be formatted differently (in which case I'd expect `upload` to fail, not `get`), or it's a bug in validation. 

That's all I've got now, but I'll try to keep digging.
pxp928: Ah so I tested with the `payload` not being required but because I thought the payload needed to be required based on the comment https://github.com/sigstore/rekor/pull/973#discussion_r945898495 I never got the e2e to work. Thanks for the help @priyawadhwa and @bobcallaway!
dlorenc: Everything is passing! Is this on track to merge this week @bobcallaway?
bobcallaway: > Everything is passing! Is this on track to merge this week @bobcallaway?

@pxp928 and I are debugging hopefully the final issue before merge now
dlorenc: YESSS!

Patch:
diff --git a/Makefile.swagger b/Makefile.swagger
index 1221342..781b4d3 100644
--- a/Makefile.swagger
+++ b/Makefile.swagger
@@ -1,2 +1,2 @@
 # This file is generated after swagger runs as part of the build; do not edit!
-SWAGGER_GEN=pkg/generated/client/entries/create_log_entry_parameters.go pkg/generated/client/entries/create_log_entry_responses.go pkg/generated/client/entries/entries_client.go pkg/generated/client/entries/get_log_entry_by_index_parameters.go pkg/generated/client/entries/get_log_entry_by_index_responses.go pkg/generated/client/entries/get_log_entry_by_uuid_parameters.go pkg/generated/client/entries/get_log_entry_by_uuid_responses.go pkg/generated/client/entries/search_log_query_parameters.go pkg/generated/client/entries/search_log_query_responses.go pkg/generated/client/index/index_client.go pkg/generated/client/index/search_index_parameters.go pkg/generated/client/index/search_index_responses.go pkg/generated/client/pubkey/get_public_key_parameters.go pkg/generated/client/pubkey/get_public_key_responses.go pkg/generated/client/pubkey/pubkey_client.go pkg/generated/client/rekor_client.go pkg/generated/client/server/get_rekor_version_parameters.go pkg/generated/client/server/get_rekor_version_responses.go pkg/generated/client/server/server_client.go pkg/generated/client/tlog/get_log_info_parameters.go pkg/generated/client/tlog/get_log_info_responses.go pkg/generated/client/tlog/get_log_proof_parameters.go pkg/generated/client/tlog/get_log_proof_responses.go pkg/generated/client/tlog/tlog_client.go pkg/generated/models/alpine.go pkg/generated/models/alpine_schema.go pkg/generated/models/alpine_v001_schema.go pkg/generated/models/consistency_proof.go pkg/generated/models/cose.go pkg/generated/models/cose_schema.go pkg/generated/models/cose_v001_schema.go pkg/generated/models/error.go pkg/generated/models/hashedrekord.go pkg/generated/models/hashedrekord_schema.go pkg/generated/models/hashedrekord_v001_schema.go pkg/generated/models/helm.go pkg/generated/models/helm_schema.go pkg/generated/models/helm_v001_schema.go pkg/generated/models/inactive_shard_log_info.go pkg/generated/models/inclusion_proof.go pkg/generated/models/intoto.go pkg/generated/models/intoto_schema.go pkg/generated/models/intoto_v001_schema.go pkg/generated/models/jar.go pkg/generated/models/jar_schema.go pkg/generated/models/jar_v001_schema.go pkg/generated/models/log_entry.go pkg/generated/models/log_info.go pkg/generated/models/proposed_entry.go pkg/generated/models/rekord.go pkg/generated/models/rekord_schema.go pkg/generated/models/rekord_v001_schema.go pkg/generated/models/rekor_version.go pkg/generated/models/rfc3161.go pkg/generated/models/rfc3161_schema.go pkg/generated/models/rfc3161_v001_schema.go pkg/generated/models/rpm.go pkg/generated/models/rpm_schema.go pkg/generated/models/rpm_v001_schema.go pkg/generated/models/search_index.go pkg/generated/models/search_log_query.go pkg/generated/models/tuf.go pkg/generated/models/tuf_schema.go pkg/generated/models/tuf_v001_schema.go pkg/generated/restapi/doc.go pkg/generated/restapi/embedded_spec.go pkg/generated/restapi/operations/entries/create_log_entry.go pkg/generated/restapi/operations/entries/create_log_entry_parameters.go pkg/generated/restapi/operations/entries/create_log_entry_responses.go pkg/generated/restapi/operations/entries/create_log_entry_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_index.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_urlbuilder.go pkg/generated/restapi/operations/entries/search_log_query.go pkg/generated/restapi/operations/entries/search_log_query_parameters.go pkg/generated/restapi/operations/entries/search_log_query_responses.go pkg/generated/restapi/operations/entries/search_log_query_urlbuilder.go pkg/generated/restapi/operations/index/search_index.go pkg/generated/restapi/operations/index/search_index_parameters.go pkg/generated/restapi/operations/index/search_index_responses.go pkg/generated/restapi/operations/index/search_index_urlbuilder.go pkg/generated/restapi/operations/pubkey/get_public_key.go pkg/generated/restapi/operations/pubkey/get_public_key_parameters.go pkg/generated/restapi/operations/pubkey/get_public_key_responses.go pkg/generated/restapi/operations/pubkey/get_public_key_urlbuilder.go pkg/generated/restapi/operations/rekor_server_api.go pkg/generated/restapi/operations/server/get_rekor_version.go pkg/generated/restapi/operations/server/get_rekor_version_parameters.go pkg/generated/restapi/operations/server/get_rekor_version_responses.go pkg/generated/restapi/operations/server/get_rekor_version_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_info.go pkg/generated/restapi/operations/tlog/get_log_info_parameters.go pkg/generated/restapi/operations/tlog/get_log_info_responses.go pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_proof.go pkg/generated/restapi/operations/tlog/get_log_proof_parameters.go pkg/generated/restapi/operations/tlog/get_log_proof_responses.go pkg/generated/restapi/operations/tlog/get_log_proof_urlbuilder.go pkg/generated/restapi/server.go
+SWAGGER_GEN=pkg/generated/client/entries/create_log_entry_parameters.go pkg/generated/client/entries/create_log_entry_responses.go pkg/generated/client/entries/entries_client.go pkg/generated/client/entries/get_log_entry_by_index_parameters.go pkg/generated/client/entries/get_log_entry_by_index_responses.go pkg/generated/client/entries/get_log_entry_by_uuid_parameters.go pkg/generated/client/entries/get_log_entry_by_uuid_responses.go pkg/generated/client/entries/search_log_query_parameters.go pkg/generated/client/entries/search_log_query_responses.go pkg/generated/client/index/index_client.go pkg/generated/client/index/search_index_parameters.go pkg/generated/client/index/search_index_responses.go pkg/generated/client/pubkey/get_public_key_parameters.go pkg/generated/client/pubkey/get_public_key_responses.go pkg/generated/client/pubkey/pubkey_client.go pkg/generated/client/rekor_client.go pkg/generated/client/server/get_rekor_version_parameters.go pkg/generated/client/server/get_rekor_version_responses.go pkg/generated/client/server/server_client.go pkg/generated/client/tlog/get_log_info_parameters.go pkg/generated/client/tlog/get_log_info_responses.go pkg/generated/client/tlog/get_log_proof_parameters.go pkg/generated/client/tlog/get_log_proof_responses.go pkg/generated/client/tlog/tlog_client.go pkg/generated/models/alpine.go pkg/generated/models/alpine_schema.go pkg/generated/models/alpine_v001_schema.go pkg/generated/models/consistency_proof.go pkg/generated/models/cose.go pkg/generated/models/cose_schema.go pkg/generated/models/cose_v001_schema.go pkg/generated/models/error.go pkg/generated/models/hashedrekord.go pkg/generated/models/hashedrekord_schema.go pkg/generated/models/hashedrekord_v001_schema.go pkg/generated/models/helm.go pkg/generated/models/helm_schema.go pkg/generated/models/helm_v001_schema.go pkg/generated/models/inactive_shard_log_info.go pkg/generated/models/inclusion_proof.go pkg/generated/models/intoto.go pkg/generated/models/intoto_schema.go pkg/generated/models/intoto_v001_schema.go pkg/generated/models/intoto_v002_schema.go pkg/generated/models/jar.go pkg/generated/models/jar_schema.go pkg/generated/models/jar_v001_schema.go pkg/generated/models/log_entry.go pkg/generated/models/log_info.go pkg/generated/models/proposed_entry.go pkg/generated/models/rekord.go pkg/generated/models/rekord_schema.go pkg/generated/models/rekord_v001_schema.go pkg/generated/models/rekor_version.go pkg/generated/models/rfc3161.go pkg/generated/models/rfc3161_schema.go pkg/generated/models/rfc3161_v001_schema.go pkg/generated/models/rpm.go pkg/generated/models/rpm_schema.go pkg/generated/models/rpm_v001_schema.go pkg/generated/models/search_index.go pkg/generated/models/search_log_query.go pkg/generated/models/tuf.go pkg/generated/models/tuf_schema.go pkg/generated/models/tuf_v001_schema.go pkg/generated/restapi/doc.go pkg/generated/restapi/embedded_spec.go pkg/generated/restapi/operations/entries/create_log_entry.go pkg/generated/restapi/operations/entries/create_log_entry_parameters.go pkg/generated/restapi/operations/entries/create_log_entry_responses.go pkg/generated/restapi/operations/entries/create_log_entry_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_index.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_index_urlbuilder.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_responses.go pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_urlbuilder.go pkg/generated/restapi/operations/entries/search_log_query.go pkg/generated/restapi/operations/entries/search_log_query_parameters.go pkg/generated/restapi/operations/entries/search_log_query_responses.go pkg/generated/restapi/operations/entries/search_log_query_urlbuilder.go pkg/generated/restapi/operations/index/search_index.go pkg/generated/restapi/operations/index/search_index_parameters.go pkg/generated/restapi/operations/index/search_index_responses.go pkg/generated/restapi/operations/index/search_index_urlbuilder.go pkg/generated/restapi/operations/pubkey/get_public_key.go pkg/generated/restapi/operations/pubkey/get_public_key_parameters.go pkg/generated/restapi/operations/pubkey/get_public_key_responses.go pkg/generated/restapi/operations/pubkey/get_public_key_urlbuilder.go pkg/generated/restapi/operations/rekor_server_api.go pkg/generated/restapi/operations/server/get_rekor_version.go pkg/generated/restapi/operations/server/get_rekor_version_parameters.go pkg/generated/restapi/operations/server/get_rekor_version_responses.go pkg/generated/restapi/operations/server/get_rekor_version_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_info.go pkg/generated/restapi/operations/tlog/get_log_info_parameters.go pkg/generated/restapi/operations/tlog/get_log_info_responses.go pkg/generated/restapi/operations/tlog/get_log_info_urlbuilder.go pkg/generated/restapi/operations/tlog/get_log_proof.go pkg/generated/restapi/operations/tlog/get_log_proof_parameters.go pkg/generated/restapi/operations/tlog/get_log_proof_responses.go pkg/generated/restapi/operations/tlog/get_log_proof_urlbuilder.go pkg/generated/restapi/server.go
diff --git a/cmd/rekor-cli/app/pflag_groups.go b/cmd/rekor-cli/app/pflag_groups.go
index 9cf7818..ccbf2be 100644
--- a/cmd/rekor-cli/app/pflag_groups.go
+++ b/cmd/rekor-cli/app/pflag_groups.go
@@ -69,7 +69,7 @@ func addArtifactPFlags(cmd *cobra.Command) error {
 			false,
 		},
 		"public-key": {
-			fileOrURLFlag,
+			multiFileOrURLFlag,
 			"path or URL to public key file",
 			false,
 		},
@@ -149,12 +149,18 @@ func CreatePropsFromPflags() *types.ArtifactProperties {
 	}
 
 	publicKeyString := viper.GetString("public-key")
-	if publicKeyString != "" {
-		if isURL(publicKeyString) {
-			props.PublicKeyPath, _ = url.Parse(publicKeyString)
-		} else {
-			props.PublicKeyPath = &url.URL{Path: publicKeyString}
+	splitPubKeyString := strings.Split(publicKeyString, ",")
+	if len(splitPubKeyString) > 0 {
+		collectedKeys := []*url.URL{}
+		for _, key := range splitPubKeyString {
+			if isURL(key) {
+				keyPath, _ := url.Parse(key)
+				collectedKeys = append(collectedKeys, keyPath)
+			} else {
+				collectedKeys = append(collectedKeys, &url.URL{Path: key})
+			}
 		}
+		props.PublicKeyPaths = collectedKeys
 	}
 
 	props.PKIFormat = viper.GetString("pki-format")
diff --git a/cmd/rekor-cli/app/pflags.go b/cmd/rekor-cli/app/pflags.go
index 21f1602..1a04b5a 100644
--- a/cmd/rekor-cli/app/pflags.go
+++ b/cmd/rekor-cli/app/pflags.go
@@ -35,20 +35,21 @@ import (
 type FlagType string
 
 const (
-	uuidFlag      FlagType = "uuid"
-	shaFlag       FlagType = "sha"
-	emailFlag     FlagType = "email"
-	operatorFlag  FlagType = "operator"
-	logIndexFlag  FlagType = "logIndex"
-	pkiFormatFlag FlagType = "pkiFormat"
-	typeFlag      FlagType = "type"
-	fileFlag      FlagType = "file"
-	urlFlag       FlagType = "url"
-	fileOrURLFlag FlagType = "fileOrURL"
-	oidFlag       FlagType = "oid"
-	formatFlag    FlagType = "format"
-	timeoutFlag   FlagType = "timeout"
-	base64Flag    FlagType = "base64"
+	uuidFlag           FlagType = "uuid"
+	shaFlag            FlagType = "sha"
+	emailFlag          FlagType = "email"
+	operatorFlag       FlagType = "operator"
+	logIndexFlag       FlagType = "logIndex"
+	pkiFormatFlag      FlagType = "pkiFormat"
+	typeFlag           FlagType = "type"
+	fileFlag           FlagType = "file"
+	urlFlag            FlagType = "url"
+	fileOrURLFlag      FlagType = "fileOrURL"
+	multiFileOrURLFlag FlagType = "multiFileOrURL"
+	oidFlag            FlagType = "oid"
+	formatFlag         FlagType = "format"
+	timeoutFlag        FlagType = "timeout"
+	base64Flag         FlagType = "base64"
 )
 
 type newPFlagValueFunc func() pflag.Value
@@ -100,6 +101,10 @@ func initializePFlagMap() {
 			// applies logic of fileFlag OR urlFlag validators from above
 			return valueFactory(fileOrURLFlag, validateFileOrURL, "")
 		},
+		multiFileOrURLFlag: func() pflag.Value {
+			// applies logic of fileFlag OR urlFlag validators from above for multi file and URL
+			return multiValueFactory(multiFileOrURLFlag, validateFileOrURL, []string{})
+		},
 		oidFlag: func() pflag.Value {
 			// this validates for an OID, which is a sequence of positive integers separated by periods
 			return valueFactory(oidFlag, validateOID, "")
@@ -142,6 +147,38 @@ func valueFactory(flagType FlagType, v validationFunc, defaultVal string) pflag.
 	}
 }
 
+func multiValueFactory(flagType FlagType, v validationFunc, defaultVal []string) pflag.Value {
+	return &multiBaseValue{
+		flagType:       flagType,
+		validationFunc: v,
+		value:          defaultVal,
+	}
+}
+
+// multiBaseValue implements pflag.Value
+type multiBaseValue struct {
+	flagType       FlagType
+	value          []string
+	validationFunc validationFunc
+}
+
+func (b *multiBaseValue) String() string {
+	return strings.Join(b.value, ",")
+}
+
+// Type returns the type of this Value
+func (b multiBaseValue) Type() string {
+	return string(b.flagType)
+}
+
+func (b *multiBaseValue) Set(value string) error {
+	if err := b.validationFunc(value); err != nil {
+		return err
+	}
+	b.value = append(b.value, value)
+	return nil
+}
+
 // baseValue implements pflag.Value
 type baseValue struct {
 	flagType       FlagType
diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go
index 400b9ac..2cb38e2 100644
--- a/cmd/rekor-cli/app/root.go
+++ b/cmd/rekor-cli/app/root.go
@@ -32,6 +32,7 @@ import (
 	_ "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
 	_ "github.com/sigstore/rekor/pkg/types/helm/v0.0.1"
 	_ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
+	_ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.2"
 	_ "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	_ "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
 	_ "github.com/sigstore/rekor/pkg/types/rfc3161/v0.0.1"
diff --git a/cmd/rekor-cli/app/search.go b/cmd/rekor-cli/app/search.go
index a33c127..1e85dbd 100644
--- a/cmd/rekor-cli/app/search.go
+++ b/cmd/rekor-cli/app/search.go
@@ -164,15 +164,20 @@ var searchCmd = &cobra.Command{
 			default:
 				return nil, fmt.Errorf("unknown pki-format %v", pkiFormat)
 			}
-			publicKeyStr := viper.GetString("public-key")
-			if isURL(publicKeyStr) {
-				params.Query.PublicKey.URL = strfmt.URI(publicKeyStr)
-			} else {
-				keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKeyStr))
-				if err != nil {
-					return nil, fmt.Errorf("error reading public key file: %w", err)
+
+			splitPubKeyString := strings.Split(publicKeyStr, ",")
+			if len(splitPubKeyString) == 1 {
+				if isURL(splitPubKeyString[0]) {
+					params.Query.PublicKey.URL = strfmt.URI(splitPubKeyString[0])
+				} else {
+					keyBytes, err := ioutil.ReadFile(filepath.Clean(splitPubKeyString[0]))
+					if err != nil {
+						return nil, fmt.Errorf("error reading public key file: %w", err)
+					}
+					params.Query.PublicKey.Content = strfmt.Base64(keyBytes)
 				}
-				params.Query.PublicKey.Content = strfmt.Base64(keyBytes)
+			} else {
+				return nil, errors.New("only one public key must be provided")
 			}
 		}
 
diff --git a/cmd/rekor-server/app/serve.go b/cmd/rekor-server/app/serve.go
index b3534ef..536443e 100644
--- a/cmd/rekor-server/app/serve.go
+++ b/cmd/rekor-server/app/serve.go
@@ -39,6 +39,7 @@ import (
 	helm_v001 "github.com/sigstore/rekor/pkg/types/helm/v0.0.1"
 	"github.com/sigstore/rekor/pkg/types/intoto"
 	intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
+	intoto_v002 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.2"
 	"github.com/sigstore/rekor/pkg/types/jar"
 	jar_v001 "github.com/sigstore/rekor/pkg/types/jar/v0.0.1"
 	"github.com/sigstore/rekor/pkg/types/rekord"
@@ -84,17 +85,17 @@ var serveCmd = &cobra.Command{
 		//TODO: add command line option to print versions supported in binary
 
 		// these trigger loading of package and therefore init() methods to run
-		pluggableTypeMap := map[string]string{
-			rekord.KIND:       rekord_v001.APIVERSION,
-			rpm.KIND:          rpm_v001.APIVERSION,
-			jar.KIND:          jar_v001.APIVERSION,
-			intoto.KIND:       intoto_v001.APIVERSION,
-			cose.KIND:         cose_v001.APIVERSION,
-			rfc3161.KIND:      rfc3161_v001.APIVERSION,
-			alpine.KIND:       alpine_v001.APIVERSION,
-			helm.KIND:         helm_v001.APIVERSION,
-			tuf.KIND:          tuf_v001.APIVERSION,
-			hashedrekord.KIND: hashedrekord_v001.APIVERSION,
+		pluggableTypeMap := map[string][]string{
+			rekord.KIND:       {rekord_v001.APIVERSION},
+			rpm.KIND:          {rpm_v001.APIVERSION},
+			jar.KIND:          {jar_v001.APIVERSION},
+			intoto.KIND:       {intoto_v001.APIVERSION, intoto_v002.APIVERSION},
+			cose.KIND:         {cose_v001.APIVERSION},
+			rfc3161.KIND:      {rfc3161_v001.APIVERSION},
+			alpine.KIND:       {alpine_v001.APIVERSION},
+			helm.KIND:         {helm_v001.APIVERSION},
+			tuf.KIND:          {tuf_v001.APIVERSION},
+			hashedrekord.KIND: {hashedrekord_v001.APIVERSION},
 		}
 
 		for k, v := range pluggableTypeMap {
diff --git a/pkg/generated/models/intoto_v002_schema.go b/pkg/generated/models/intoto_v002_schema.go
new file mode 100644
index 0000000..3e3b7bb
--- /dev/null
+++ b/pkg/generated/models/intoto_v002_schema.go
@@ -0,0 +1,724 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"encoding/json"
+	"strconv"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// IntotoV002Schema intoto v0.0.2 Schema
+//
+// Schema for intoto object
+//
+// swagger:model intotoV002Schema
+type IntotoV002Schema struct {
+
+	// content
+	// Required: true
+	Content *IntotoV002SchemaContent `json:"content"`
+}
+
+// Validate validates this intoto v002 schema
+func (m *IntotoV002Schema) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateContent(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002Schema) validateContent(formats strfmt.Registry) error {
+
+	if err := validate.Required("content", "body", m.Content); err != nil {
+		return err
+	}
+
+	if m.Content != nil {
+		if err := m.Content.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v002 schema based on the context it is used
+func (m *IntotoV002Schema) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateContent(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002Schema) contextValidateContent(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Content != nil {
+		if err := m.Content.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV002Schema) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV002Schema) UnmarshalBinary(b []byte) error {
+	var res IntotoV002Schema
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV002SchemaContent intoto v002 schema content
+//
+// swagger:model IntotoV002SchemaContent
+type IntotoV002SchemaContent struct {
+
+	// envelope
+	Envelope *IntotoV002SchemaContentEnvelope `json:"envelope,omitempty"`
+
+	// hash
+	Hash *IntotoV002SchemaContentHash `json:"hash,omitempty"`
+
+	// payload hash
+	PayloadHash *IntotoV002SchemaContentPayloadHash `json:"payloadHash,omitempty"`
+}
+
+// Validate validates this intoto v002 schema content
+func (m *IntotoV002SchemaContent) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateEnvelope(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateHash(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validatePayloadHash(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContent) validateEnvelope(formats strfmt.Registry) error {
+	if swag.IsZero(m.Envelope) { // not required
+		return nil
+	}
+
+	if m.Envelope != nil {
+		if err := m.Envelope.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "envelope")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content" + "." + "envelope")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContent) validateHash(formats strfmt.Registry) error {
+	if swag.IsZero(m.Hash) { // not required
+		return nil
+	}
+
+	if m.Hash != nil {
+		if err := m.Hash.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "hash")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content" + "." + "hash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContent) validatePayloadHash(formats strfmt.Registry) error {
+	if swag.IsZero(m.PayloadHash) { // not required
+		return nil
+	}
+
+	if m.PayloadHash != nil {
+		if err := m.PayloadHash.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "payloadHash")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content" + "." + "payloadHash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v002 schema content based on the context it is used
+func (m *IntotoV002SchemaContent) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateEnvelope(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.contextValidateHash(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.contextValidatePayloadHash(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContent) contextValidateEnvelope(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Envelope != nil {
+		if err := m.Envelope.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "envelope")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content" + "." + "envelope")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContent) contextValidateHash(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.Hash != nil {
+		if err := m.Hash.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "hash")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content" + "." + "hash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContent) contextValidatePayloadHash(ctx context.Context, formats strfmt.Registry) error {
+
+	if m.PayloadHash != nil {
+		if err := m.PayloadHash.ContextValidate(ctx, formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("content" + "." + "payloadHash")
+			} else if ce, ok := err.(*errors.CompositeError); ok {
+				return ce.ValidateName("content" + "." + "payloadHash")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV002SchemaContent) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV002SchemaContent) UnmarshalBinary(b []byte) error {
+	var res IntotoV002SchemaContent
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV002SchemaContentEnvelope dsse envelope
+//
+// swagger:model IntotoV002SchemaContentEnvelope
+type IntotoV002SchemaContentEnvelope struct {
+
+	// payload of the envelope
+	// Format: byte
+	Payload strfmt.Base64 `json:"payload,omitempty"`
+
+	// type describing the payload
+	// Required: true
+	PayloadType *string `json:"payloadType"`
+
+	// collection of all signatures of the envelope's payload
+	// Required: true
+	// Min Items: 1
+	Signatures []*IntotoV002SchemaContentEnvelopeSignaturesItems0 `json:"signatures"`
+}
+
+// Validate validates this intoto v002 schema content envelope
+func (m *IntotoV002SchemaContentEnvelope) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validatePayloadType(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateSignatures(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContentEnvelope) validatePayloadType(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"envelope"+"."+"payloadType", "body", m.PayloadType); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContentEnvelope) validateSignatures(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"envelope"+"."+"signatures", "body", m.Signatures); err != nil {
+		return err
+	}
+
+	iSignaturesSize := int64(len(m.Signatures))
+
+	if err := validate.MinItems("content"+"."+"envelope"+"."+"signatures", "body", iSignaturesSize, 1); err != nil {
+		return err
+	}
+
+	for i := 0; i < len(m.Signatures); i++ {
+		if swag.IsZero(m.Signatures[i]) { // not required
+			continue
+		}
+
+		if m.Signatures[i] != nil {
+			if err := m.Signatures[i].Validate(formats); err != nil {
+				if ve, ok := err.(*errors.Validation); ok {
+					return ve.ValidateName("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i))
+				} else if ce, ok := err.(*errors.CompositeError); ok {
+					return ce.ValidateName("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i))
+				}
+				return err
+			}
+		}
+
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v002 schema content envelope based on the context it is used
+func (m *IntotoV002SchemaContentEnvelope) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidateSignatures(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContentEnvelope) contextValidateSignatures(ctx context.Context, formats strfmt.Registry) error {
+
+	for i := 0; i < len(m.Signatures); i++ {
+
+		if m.Signatures[i] != nil {
+			if err := m.Signatures[i].ContextValidate(ctx, formats); err != nil {
+				if ve, ok := err.(*errors.Validation); ok {
+					return ve.ValidateName("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i))
+				} else if ce, ok := err.(*errors.CompositeError); ok {
+					return ce.ValidateName("content" + "." + "envelope" + "." + "signatures" + "." + strconv.Itoa(i))
+				}
+				return err
+			}
+		}
+
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV002SchemaContentEnvelope) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV002SchemaContentEnvelope) UnmarshalBinary(b []byte) error {
+	var res IntotoV002SchemaContentEnvelope
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV002SchemaContentEnvelopeSignaturesItems0 a signature of the envelope's payload along with the public key for the signature
+//
+// swagger:model IntotoV002SchemaContentEnvelopeSignaturesItems0
+type IntotoV002SchemaContentEnvelopeSignaturesItems0 struct {
+
+	// optional id of the key used to create the signature
+	Keyid string `json:"keyid,omitempty"`
+
+	// public key that corresponds to this signature
+	// Read Only: true
+	// Format: byte
+	PublicKey strfmt.Base64 `json:"publicKey,omitempty"`
+
+	// signature of the payload
+	// Format: byte
+	Sig strfmt.Base64 `json:"sig,omitempty"`
+}
+
+// Validate validates this intoto v002 schema content envelope signatures items0
+func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) Validate(formats strfmt.Registry) error {
+	return nil
+}
+
+// ContextValidate validate this intoto v002 schema content envelope signatures items0 based on the context it is used
+func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.contextValidatePublicKey(ctx, formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) contextValidatePublicKey(ctx context.Context, formats strfmt.Registry) error {
+
+	if err := validate.ReadOnly(ctx, "publicKey", "body", strfmt.Base64(m.PublicKey)); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV002SchemaContentEnvelopeSignaturesItems0) UnmarshalBinary(b []byte) error {
+	var res IntotoV002SchemaContentEnvelopeSignaturesItems0
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV002SchemaContentHash Specifies the hash algorithm and value encompassing the entire signed envelope
+//
+// swagger:model IntotoV002SchemaContentHash
+type IntotoV002SchemaContentHash struct {
+
+	// The hashing function used to compute the hash value
+	// Required: true
+	// Enum: [sha256]
+	Algorithm *string `json:"algorithm"`
+
+	// The hash value for the archive
+	// Required: true
+	Value *string `json:"value"`
+}
+
+// Validate validates this intoto v002 schema content hash
+func (m *IntotoV002SchemaContentHash) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateAlgorithm(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateValue(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+var intotoV002SchemaContentHashTypeAlgorithmPropEnum []interface{}
+
+func init() {
+	var res []string
+	if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil {
+		panic(err)
+	}
+	for _, v := range res {
+		intotoV002SchemaContentHashTypeAlgorithmPropEnum = append(intotoV002SchemaContentHashTypeAlgorithmPropEnum, v)
+	}
+}
+
+const (
+
+	// IntotoV002SchemaContentHashAlgorithmSha256 captures enum value "sha256"
+	IntotoV002SchemaContentHashAlgorithmSha256 string = "sha256"
+)
+
+// prop value enum
+func (m *IntotoV002SchemaContentHash) validateAlgorithmEnum(path, location string, value string) error {
+	if err := validate.EnumCase(path, location, value, intotoV002SchemaContentHashTypeAlgorithmPropEnum, true); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContentHash) validateAlgorithm(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"hash"+"."+"algorithm", "body", m.Algorithm); err != nil {
+		return err
+	}
+
+	// value enum
+	if err := m.validateAlgorithmEnum("content"+"."+"hash"+"."+"algorithm", "body", *m.Algorithm); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContentHash) validateValue(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"hash"+"."+"value", "body", m.Value); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v002 schema content hash based on the context it is used
+func (m *IntotoV002SchemaContentHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV002SchemaContentHash) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV002SchemaContentHash) UnmarshalBinary(b []byte) error {
+	var res IntotoV002SchemaContentHash
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
+
+// IntotoV002SchemaContentPayloadHash Specifies the hash algorithm and value covering the payload within the DSSE envelope
+//
+// swagger:model IntotoV002SchemaContentPayloadHash
+type IntotoV002SchemaContentPayloadHash struct {
+
+	// The hashing function used to compute the hash value
+	// Required: true
+	// Enum: [sha256]
+	Algorithm *string `json:"algorithm"`
+
+	// The hash value of the payload
+	// Required: true
+	Value *string `json:"value"`
+}
+
+// Validate validates this intoto v002 schema content payload hash
+func (m *IntotoV002SchemaContentPayloadHash) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateAlgorithm(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateValue(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+var intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum []interface{}
+
+func init() {
+	var res []string
+	if err := json.Unmarshal([]byte(`["sha256"]`), &res); err != nil {
+		panic(err)
+	}
+	for _, v := range res {
+		intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum = append(intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum, v)
+	}
+}
+
+const (
+
+	// IntotoV002SchemaContentPayloadHashAlgorithmSha256 captures enum value "sha256"
+	IntotoV002SchemaContentPayloadHashAlgorithmSha256 string = "sha256"
+)
+
+// prop value enum
+func (m *IntotoV002SchemaContentPayloadHash) validateAlgorithmEnum(path, location string, value string) error {
+	if err := validate.EnumCase(path, location, value, intotoV002SchemaContentPayloadHashTypeAlgorithmPropEnum, true); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *IntotoV002SchemaContentPayloadHash) validateAlgorithm(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"payloadHash"+"."+"algorithm", "body", m.Algorithm); err != nil {
+		return err
+	}
+
+	// value enum
+	if err := m.validateAlgorithmEnum("content"+"."+"payloadHash"+"."+"algorithm", "body", *m.Algorithm); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *IntotoV002SchemaContentPayloadHash) validateValue(formats strfmt.Registry) error {
+
+	if err := validate.Required("content"+"."+"payloadHash"+"."+"value", "body", m.Value); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ContextValidate validate this intoto v002 schema content payload hash based on the context it is used
+func (m *IntotoV002SchemaContentPayloadHash) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+	var res []error
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *IntotoV002SchemaContentPayloadHash) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *IntotoV002SchemaContentPayloadHash) UnmarshalBinary(b []byte) error {
+	var res IntotoV002SchemaContentPayloadHash
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go
index ef89e89..6744d7c 100644
--- a/pkg/generated/restapi/embedded_spec.go
+++ b/pkg/generated/restapi/embedded_spec.go
@@ -1955,6 +1955,176 @@ func init() {
       },
       "readOnly": true
     },
+    "IntotoV002SchemaContent": {
+      "type": "object",
+      "properties": {
+        "envelope": {
+          "description": "dsse envelope",
+          "type": "object",
+          "required": [
+            "payloadType",
+            "signatures"
+          ],
+          "properties": {
+            "payload": {
+              "description": "payload of the envelope",
+              "type": "string",
+              "format": "byte",
+              "writeOnly": true
+            },
+            "payloadType": {
+              "description": "type describing the payload",
+              "type": "string"
+            },
+            "signatures": {
+              "description": "collection of all signatures of the envelope's payload",
+              "type": "array",
+              "minItems": 1,
+              "items": {
+                "$ref": "#/definitions/IntotoV002SchemaContentEnvelopeSignaturesItems0"
+              }
+            }
+          }
+        },
+        "hash": {
+          "description": "Specifies the hash algorithm and value encompassing the entire signed envelope",
+          "type": "object",
+          "required": [
+            "algorithm",
+            "value"
+          ],
+          "properties": {
+            "algorithm": {
+              "description": "The hashing function used to compute the hash value",
+              "type": "string",
+              "enum": [
+                "sha256"
+              ]
+            },
+            "value": {
+              "description": "The hash value for the archive",
+              "type": "string"
+            }
+          },
+          "readOnly": true
+        },
+        "payloadHash": {
+          "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope",
+          "type": "object",
+          "required": [
+            "algorithm",
+            "value"
+          ],
+          "properties": {
+            "algorithm": {
+              "description": "The hashing function used to compute the hash value",
+              "type": "string",
+              "enum": [
+                "sha256"
+              ]
+            },
+            "value": {
+              "description": "The hash value of the payload",
+              "type": "string"
+            }
+          },
+          "readOnly": true
+        }
+      }
+    },
+    "IntotoV002SchemaContentEnvelope": {
+      "description": "dsse envelope",
+      "type": "object",
+      "required": [
+        "payloadType",
+        "signatures"
+      ],
+      "properties": {
+        "payload": {
+          "description": "payload of the envelope",
+          "type": "string",
+          "format": "byte",
+          "writeOnly": true
+        },
+        "payloadType": {
+          "description": "type describing the payload",
+          "type": "string"
+        },
+        "signatures": {
+          "description": "collection of all signatures of the envelope's payload",
+          "type": "array",
+          "minItems": 1,
+          "items": {
+            "$ref": "#/definitions/IntotoV002SchemaContentEnvelopeSignaturesItems0"
+          }
+        }
+      }
+    },
+    "IntotoV002SchemaContentEnvelopeSignaturesItems0": {
+      "description": "a signature of the envelope's payload along with the public key for the signature",
+      "type": "object",
+      "properties": {
+        "keyid": {
+          "description": "optional id of the key used to create the signature",
+          "type": "string"
+        },
+        "publicKey": {
+          "description": "public key that corresponds to this signature",
+          "type": "string",
+          "format": "byte",
+          "readOnly": true
+        },
+        "sig": {
+          "description": "signature of the payload",
+          "type": "string",
+          "format": "byte"
+        }
+      }
+    },
+    "IntotoV002SchemaContentHash": {
+      "description": "Specifies the hash algorithm and value encompassing the entire signed envelope",
+      "type": "object",
+      "required": [
+        "algorithm",
+        "value"
+      ],
+      "properties": {
+        "algorithm": {
+          "description": "The hashing function used to compute the hash value",
+          "type": "string",
+          "enum": [
+            "sha256"
+          ]
+        },
+        "value": {
+          "description": "The hash value for the archive",
+          "type": "string"
+        }
+      },
+      "readOnly": true
+    },
+    "IntotoV002SchemaContentPayloadHash": {
+      "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope",
+      "type": "object",
+      "required": [
+        "algorithm",
+        "value"
+      ],
+      "properties": {
+        "algorithm": {
+          "description": "The hashing function used to compute the hash value",
+          "type": "string",
+          "enum": [
+            "sha256"
+          ]
+        },
+        "value": {
+          "description": "The hash value of the payload",
+          "type": "string"
+        }
+      },
+      "readOnly": true
+    },
     "JarV001SchemaArchive": {
       "description": "Information about the archive associated with the entry",
       "type": "object",
@@ -3066,6 +3236,9 @@ func init() {
       "oneOf": [
         {
           "$ref": "#/definitions/intotoV001Schema"
+        },
+        {
+          "$ref": "#/definitions/intotoV002Schema"
         }
       ],
       "$schema": "http://json-schema.org/draft-07/schema",
@@ -3143,6 +3316,95 @@ func init() {
       "$schema": "http://json-schema.org/draft-07/schema",
       "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_1_schema.json"
     },
+    "intotoV002Schema": {
+      "description": "Schema for intoto object",
+      "type": "object",
+      "title": "intoto v0.0.2 Schema",
+      "required": [
+        "content"
+      ],
+      "properties": {
+        "content": {
+          "type": "object",
+          "properties": {
+            "envelope": {
+              "description": "dsse envelope",
+              "type": "object",
+              "required": [
+                "payloadType",
+                "signatures"
+              ],
+              "properties": {
+                "payload": {
+                  "description": "payload of the envelope",
+                  "type": "string",
+                  "format": "byte",
+                  "writeOnly": true
+                },
+                "payloadType": {
+                  "description": "type describing the payload",
+                  "type": "string"
+                },
+                "signatures": {
+                  "description": "collection of all signatures of the envelope's payload",
+                  "type": "array",
+                  "minItems": 1,
+                  "items": {
+                    "$ref": "#/definitions/IntotoV002SchemaContentEnvelopeSignaturesItems0"
+                  }
+                }
+              }
+            },
+            "hash": {
+              "description": "Specifies the hash algorithm and value encompassing the entire signed envelope",
+              "type": "object",
+              "required": [
+                "algorithm",
+                "value"
+              ],
+              "properties": {
+                "algorithm": {
+                  "description": "The hashing function used to compute the hash value",
+                  "type": "string",
+                  "enum": [
+                    "sha256"
+                  ]
+                },
+                "value": {
+                  "description": "The hash value for the archive",
+                  "type": "string"
+                }
+              },
+              "readOnly": true
+            },
+            "payloadHash": {
+              "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope",
+              "type": "object",
+              "required": [
+                "algorithm",
+                "value"
+              ],
+              "properties": {
+                "algorithm": {
+                  "description": "The hashing function used to compute the hash value",
+                  "type": "string",
+                  "enum": [
+                    "sha256"
+                  ]
+                },
+                "value": {
+                  "description": "The hash value of the payload",
+                  "type": "string"
+                }
+              },
+              "readOnly": true
+            }
+          }
+        }
+      },
+      "$schema": "http://json-schema.org/draft-07/schema",
+      "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_2_schema.json"
+    },
     "jar": {
       "description": "Java Archive (JAR)",
       "type": "object",
diff --git a/pkg/types/alpine/v0.0.1/entry.go b/pkg/types/alpine/v0.0.1/entry.go
index 3a68ceb..09da6c9 100644
--- a/pkg/types/alpine/v0.0.1/entry.go
+++ b/pkg/types/alpine/v0.0.1/entry.go
@@ -322,16 +322,21 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types
 
 	re.AlpineModel.PublicKey = &models.AlpineV001SchemaPublicKey{}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided")
+		}
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
-		re.AlpineModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
-	} else {
-		re.AlpineModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
 
+	re.AlpineModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0])
+
 	if err := re.validate(); err != nil {
 		return nil, err
 	}
diff --git a/pkg/types/cose/v0.0.1/entry.go b/pkg/types/cose/v0.0.1/entry.go
index 7ddb5a5..48b4296 100644
--- a/pkg/types/cose/v0.0.1/entry.go
+++ b/pkg/types/cose/v0.0.1/entry.go
@@ -316,19 +316,20 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A
 		}
 	}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		if props.PublicKeyPath == nil {
-			return nil, errors.New("public key must be provided to verify signature")
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided to verify signature")
 		}
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
-	if err != nil {
-		return nil, err
-	}
-	kb := strfmt.Base64(publicKeyBytes)
+
+	kb := strfmt.Base64(publicKeyBytes[0])
 	mb := strfmt.Base64(messageBytes)
 
 	re := V001Entry{
diff --git a/pkg/types/entries.go b/pkg/types/entries.go
index cda7f27..5555975 100644
--- a/pkg/types/entries.go
+++ b/pkg/types/entries.go
@@ -152,7 +152,7 @@ type ArtifactProperties struct {
 	ArtifactBytes               []byte
 	SignaturePath               *url.URL
 	SignatureBytes              []byte
-	PublicKeyPath               *url.URL
-	PublicKeyBytes              []byte
+	PublicKeyPaths              []*url.URL
+	PublicKeyBytes              [][]byte
 	PKIFormat                   string
 }
diff --git a/pkg/types/hashedrekord/v0.0.1/entry.go b/pkg/types/hashedrekord/v0.0.1/entry.go
index aea92fc..74d6654 100644
--- a/pkg/types/hashedrekord/v0.0.1/entry.go
+++ b/pkg/types/hashedrekord/v0.0.1/entry.go
@@ -217,17 +217,20 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types
 
 	re.HashedRekordObj.Signature.PublicKey = &models.HashedrekordV001SchemaSignaturePublicKey{}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		if props.PublicKeyPath == nil {
-			return nil, errors.New("public key must be provided to verify detached signature")
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided to verify detached signature")
 		}
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
-	re.HashedRekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes)
 
+	re.HashedRekordObj.Signature.PublicKey.Content = strfmt.Base64(publicKeyBytes[0])
 	re.HashedRekordObj.Data.Hash = &models.HashedrekordV001SchemaDataHash{
 		Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256),
 		Value:     swag.String(props.ArtifactHash),
diff --git a/pkg/types/helm/v0.0.1/entry.go b/pkg/types/helm/v0.0.1/entry.go
index 55f68fe..000cb2c 100644
--- a/pkg/types/helm/v0.0.1/entry.go
+++ b/pkg/types/helm/v0.0.1/entry.go
@@ -317,16 +317,20 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types
 
 	re.HelmObj.PublicKey = &models.HelmV001SchemaPublicKey{}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided")
+		}
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
-		re.HelmObj.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
-	} else {
-		re.HelmObj.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
 
+	re.HelmObj.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0])
 	if err := re.validate(); err != nil {
 		return nil, err
 	}
diff --git a/pkg/types/intoto/intoto.go b/pkg/types/intoto/intoto.go
index f48daac..81d2cee 100644
--- a/pkg/types/intoto/intoto.go
+++ b/pkg/types/intoto/intoto.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/types"
+	"golang.org/x/exp/slices"
 )
 
 const (
@@ -70,5 +71,17 @@ func (it *BaseIntotoType) CreateProposedEntry(ctx context.Context, version strin
 }
 
 func (it BaseIntotoType) DefaultVersion() string {
-	return "0.0.1"
+	return "0.0.2"
+}
+
+// SupportedVersions returns the supported versions for this type;
+// it deliberately omits 0.0.1 from the list of supported versions as that
+// version did not persist signatures inside the log entry
+func (it BaseIntotoType) SupportedVersions() []string {
+	return []string{"0.0.2"}
+}
+
+// IsSupportedVersion returns true if the version can be inserted into the log, and false if not
+func (it *BaseIntotoType) IsSupportedVersion(proposedVersion string) bool {
+	return slices.Contains(it.SupportedVersions(), proposedVersion)
 }
diff --git a/pkg/types/intoto/intoto_schema.json b/pkg/types/intoto/intoto_schema.json
index b99b3c2..16f6172 100644
--- a/pkg/types/intoto/intoto_schema.json
+++ b/pkg/types/intoto/intoto_schema.json
@@ -7,6 +7,9 @@
     "oneOf": [
         {
             "$ref": "v0.0.1/intoto_v0_0_1_schema.json"
+        },
+        {
+            "$ref": "v0.0.2/intoto_v0_0_2_schema.json"
         }
     ]
 }
diff --git a/pkg/types/intoto/v0.0.1/entry.go b/pkg/types/intoto/v0.0.1/entry.go
index 49e42cc..438f083 100644
--- a/pkg/types/intoto/v0.0.1/entry.go
+++ b/pkg/types/intoto/v0.0.1/entry.go
@@ -292,16 +292,20 @@ func (v V001Entry) CreateFromArtifactProperties(_ context.Context, props types.A
 		}
 	}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		if props.PublicKeyPath == nil {
-			return nil, errors.New("public key must be provided to verify signature")
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided to verify signature")
 		}
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
-	kb := strfmt.Base64(publicKeyBytes)
+
+	kb := strfmt.Base64(publicKeyBytes[0])
 
 	re := V001Entry{
 		IntotoObj: models.IntotoV001Schema{
diff --git a/pkg/types/intoto/v0.0.2/entry.go b/pkg/types/intoto/v0.0.2/entry.go
new file mode 100644
index 0000000..7313cce
--- /dev/null
+++ b/pkg/types/intoto/v0.0.2/entry.go
@@ -0,0 +1,423 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package intoto
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+
+	"github.com/in-toto/in-toto-golang/in_toto"
+	"github.com/secure-systems-lab/go-securesystemslib/dsse"
+	"github.com/spf13/viper"
+
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/log"
+	"github.com/sigstore/rekor/pkg/pki/x509"
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/rekor/pkg/types/intoto"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+const (
+	APIVERSION = "0.0.2"
+)
+
+func init() {
+	if err := intoto.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil {
+		log.Logger.Panic(err)
+	}
+}
+
+type V002Entry struct {
+	IntotoObj models.IntotoV002Schema
+	env       dsse.Envelope
+}
+
+func (v V002Entry) APIVersion() string {
+	return APIVERSION
+}
+
+func NewEntry() types.EntryImpl {
+	return &V002Entry{}
+}
+
+func (v V002Entry) IndexKeys() ([]string, error) {
+	var result []string
+
+	if v.IntotoObj.Content == nil || v.IntotoObj.Content.Envelope == nil {
+		log.Logger.Info("IntotoObj content or dsse envelope is nil")
+		return result, nil
+	}
+
+	for _, sig := range v.IntotoObj.Content.Envelope.Signatures {
+		keyObj, err := x509.NewPublicKey(bytes.NewReader(sig.PublicKey))
+		if err != nil {
+			return result, err
+		}
+
+		canonKey, err := keyObj.CanonicalValue()
+		if err != nil {
+			return result, fmt.Errorf("could not canonicize key: %w", err)
+		}
+
+		keyHash := sha256.Sum256(canonKey)
+		result = append(result, "sha256:"+hex.EncodeToString(keyHash[:]))
+
+		result = append(result, keyObj.Subjects()...)
+	}
+
+	payloadKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.IntotoObj.Content.PayloadHash.Algorithm, *v.IntotoObj.Content.PayloadHash.Value))
+	result = append(result, payloadKey)
+
+	hashkey := strings.ToLower(fmt.Sprintf("%s:%s", *v.IntotoObj.Content.Hash.Algorithm, *v.IntotoObj.Content.Hash.Value))
+	result = append(result, hashkey)
+
+	switch *v.IntotoObj.Content.Envelope.PayloadType {
+	case in_toto.PayloadType:
+
+		if v.IntotoObj.Content.Envelope.Payload == nil {
+			log.Logger.Info("IntotoObj DSSE payload is empty")
+			return result, nil
+		}
+		decodedPayload, err := base64.StdEncoding.DecodeString(string(v.IntotoObj.Content.Envelope.Payload))
+		if err != nil {
+			return result, fmt.Errorf("could not decode envelope payload: %w", err)
+		}
+		statement, err := parseStatement(decodedPayload)
+		if err != nil {
+			return result, err
+		}
+		for _, s := range statement.Subject {
+			for alg, ds := range s.Digest {
+				result = append(result, alg+":"+ds)
+			}
+		}
+		// Not all in-toto statements will contain a SLSA provenance predicate.
+		// See https://github.com/in-toto/attestation/blob/main/spec/README.md#predicate
+		// for other predicates.
+		if predicate, err := parseSlsaPredicate(decodedPayload); err == nil {
+			if predicate.Predicate.Materials != nil {
+				for _, s := range predicate.Predicate.Materials {
+					for alg, ds := range s.Digest {
+						result = append(result, alg+":"+ds)
+					}
+				}
+			}
+		}
+	default:
+		log.Logger.Infof("Unknown in_toto DSSE envelope Type: %s", *v.IntotoObj.Content.Envelope.PayloadType)
+	}
+	return result, nil
+}
+
+func parseStatement(p []byte) (*in_toto.Statement, error) {
+	ps := in_toto.Statement{}
+	if err := json.Unmarshal(p, &ps); err != nil {
+		return nil, err
+	}
+	return &ps, nil
+}
+
+func parseSlsaPredicate(p []byte) (*in_toto.ProvenanceStatement, error) {
+	predicate := in_toto.ProvenanceStatement{}
+	if err := json.Unmarshal(p, &predicate); err != nil {
+		return nil, err
+	}
+	return &predicate, nil
+}
+
+func (v *V002Entry) Unmarshal(pe models.ProposedEntry) error {
+	it, ok := pe.(*models.Intoto)
+	if !ok {
+		return errors.New("cannot unmarshal non Intoto v0.0.2 type")
+	}
+
+	var err error
+	if err := types.DecodeEntry(it.Spec, &v.IntotoObj); err != nil {
+		return err
+	}
+
+	// field validation
+	if err := v.IntotoObj.Validate(strfmt.Default); err != nil {
+		return err
+	}
+
+	if string(v.IntotoObj.Content.Envelope.Payload) == "" {
+		return nil
+	}
+
+	env := &dsse.Envelope{
+		Payload:     string(v.IntotoObj.Content.Envelope.Payload),
+		PayloadType: *v.IntotoObj.Content.Envelope.PayloadType,
+	}
+
+	allPubKeyBytes := make([][]byte, 0)
+	for _, sig := range v.IntotoObj.Content.Envelope.Signatures {
+		env.Signatures = append(env.Signatures, dsse.Signature{
+			KeyID: sig.Keyid,
+			Sig:   string(sig.Sig),
+		})
+
+		allPubKeyBytes = append(allPubKeyBytes, sig.PublicKey)
+	}
+
+	if _, err := verifyEnvelope(allPubKeyBytes, env); err != nil {
+		return err
+	}
+
+	v.env = *env
+
+	decodedPayload, err := base64.StdEncoding.DecodeString(string(v.IntotoObj.Content.Envelope.Payload))
+	if err != nil {
+		return fmt.Errorf("could not decode envelope payload: %w", err)
+	}
+
+	h := sha256.Sum256(decodedPayload)
+	v.IntotoObj.Content.PayloadHash = &models.IntotoV002SchemaContentPayloadHash{
+		Algorithm: swag.String(models.IntotoV002SchemaContentPayloadHashAlgorithmSha256),
+		Value:     swag.String(hex.EncodeToString(h[:])),
+	}
+
+	return nil
+}
+
+func (v *V002Entry) Canonicalize(ctx context.Context) ([]byte, error) {
+
+	canonicalEntry := models.IntotoV002Schema{
+		Content: &models.IntotoV002SchemaContent{
+			Envelope: &models.IntotoV002SchemaContentEnvelope{
+				PayloadType: v.IntotoObj.Content.Envelope.PayloadType,
+				Signatures:  v.IntotoObj.Content.Envelope.Signatures,
+			},
+			Hash:        v.IntotoObj.Content.Hash,
+			PayloadHash: v.IntotoObj.Content.PayloadHash,
+		},
+	}
+	itObj := models.Intoto{}
+	itObj.APIVersion = swag.String(APIVERSION)
+	itObj.Spec = &canonicalEntry
+
+	return json.Marshal(&itObj)
+}
+
+// AttestationKey returns the digest of the attestation that was uploaded, to be used to lookup the attestation from storage
+func (v *V002Entry) AttestationKey() string {
+	if v.IntotoObj.Content != nil && v.IntotoObj.Content.PayloadHash != nil {
+		return fmt.Sprintf("%s:%s", *v.IntotoObj.Content.PayloadHash.Algorithm, *v.IntotoObj.Content.PayloadHash.Value)
+	}
+	return ""
+}
+
+// AttestationKeyValue returns both the key and value to be persisted into attestation storage
+func (v *V002Entry) AttestationKeyValue() (string, []byte) {
+	storageSize := base64.StdEncoding.DecodedLen(len(v.env.Payload))
+	if storageSize > viper.GetInt("max_attestation_size") {
+		log.Logger.Infof("Skipping attestation storage, size %d is greater than max %d", storageSize, viper.GetInt("max_attestation_size"))
+		return "", nil
+	}
+	attBytes, err := base64.StdEncoding.DecodeString(v.env.Payload)
+	if err != nil {
+		log.Logger.Infof("could not decode envelope payload: %w", err)
+		return "", nil
+	}
+	return v.AttestationKey(), attBytes
+}
+
+type verifier struct {
+	s signature.Signer
+	v signature.Verifier
+}
+
+func (v *verifier) KeyID() (string, error) {
+	return "", nil
+}
+
+func (v *verifier) Public() crypto.PublicKey {
+	// the dsse library uses this to generate a key ID if the KeyID function returns an empty string
+	// as well for the AcceptedKey return value.  Unfortunately since key ids can be arbitrary, we don't
+	// know how to generate a matching id for the key id on the envelope's signature...
+	// dsse verify will skip verifiers whose key id doesn't match the signature's key id, unless it fails
+	// to generate one from the public key... so we trick it by returning nil ¯\_(ツ)_/¯
+	return nil
+}
+
+func (v *verifier) Sign(data []byte) (sig []byte, err error) {
+	if v.s == nil {
+		return nil, errors.New("nil signer")
+	}
+	sig, err = v.s.SignMessage(bytes.NewReader(data), options.WithCryptoSignerOpts(crypto.SHA256))
+	if err != nil {
+		return nil, err
+	}
+	return sig, nil
+}
+
+func (v *verifier) Verify(data, sig []byte) error {
+	if v.v == nil {
+		return errors.New("nil verifier")
+	}
+	return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+}
+
+func (v V002Entry) CreateFromArtifactProperties(_ context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
+	returnVal := models.Intoto{}
+	re := V002Entry{
+		IntotoObj: models.IntotoV002Schema{
+			Content: &models.IntotoV002SchemaContent{
+				Envelope: &models.IntotoV002SchemaContentEnvelope{},
+			},
+		}}
+	var err error
+	artifactBytes := props.ArtifactBytes
+	if artifactBytes == nil {
+		if props.ArtifactPath == nil {
+			return nil, errors.New("path to artifact file must be specified")
+		}
+		if props.ArtifactPath.IsAbs() {
+			return nil, errors.New("intoto envelopes cannot be fetched over HTTP(S)")
+		}
+		artifactBytes, err = ioutil.ReadFile(filepath.Clean(props.ArtifactPath.Path))
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	env := dsse.Envelope{}
+	if err := json.Unmarshal(artifactBytes, &env); err != nil {
+		return nil, fmt.Errorf("payload must be a valid dsse envelope: %w", err)
+	}
+
+	allPubKeyBytes := make([][]byte, 0)
+	if len(props.PublicKeyBytes) > 0 {
+		allPubKeyBytes = append(allPubKeyBytes, props.PublicKeyBytes...)
+	}
+
+	if len(props.PublicKeyPaths) > 0 {
+		for _, path := range props.PublicKeyPaths {
+			if path.IsAbs() {
+				return nil, errors.New("dsse public keys cannot be fetched over HTTP(S)")
+			}
+
+			publicKeyBytes, err := ioutil.ReadFile(filepath.Clean(path.Path))
+			if err != nil {
+				return nil, fmt.Errorf("error reading public key file: %w", err)
+			}
+
+			allPubKeyBytes = append(allPubKeyBytes, publicKeyBytes)
+		}
+	}
+
+	keysBySig, err := verifyEnvelope(allPubKeyBytes, &env)
+	if err != nil {
+		return nil, err
+	}
+
+	b64 := strfmt.Base64([]byte(env.Payload))
+	re.IntotoObj.Content.Envelope.Payload = b64
+	re.IntotoObj.Content.Envelope.PayloadType = &env.PayloadType
+
+	for _, sig := range env.Signatures {
+		key, ok := keysBySig[sig.Sig]
+		if !ok {
+			return nil, errors.New("all signatures must have a key that verifies it")
+		}
+
+		canonKey, err := key.CanonicalValue()
+		if err != nil {
+			return nil, fmt.Errorf("could not canonicize key: %w", err)
+		}
+
+		keyBytes := strfmt.Base64(canonKey)
+		re.IntotoObj.Content.Envelope.Signatures = append(re.IntotoObj.Content.Envelope.Signatures, &models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+			Keyid:     sig.KeyID,
+			Sig:       strfmt.Base64([]byte(sig.Sig)),
+			PublicKey: keyBytes,
+		})
+	}
+
+	h := sha256.Sum256([]byte(artifactBytes))
+	re.IntotoObj.Content.Hash = &models.IntotoV002SchemaContentHash{
+		Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256),
+		Value:     swag.String(hex.EncodeToString(h[:])),
+	}
+
+	returnVal.Spec = re.IntotoObj
+	returnVal.APIVersion = swag.String(re.APIVersion())
+
+	return &returnVal, nil
+}
+
+// verifyEnvelope takes in an array of possible key bytes and attempts to parse them as x509 public keys.
+// it then uses these to verify the envelope and makes sure that every signature on the envelope is verified.
+// it returns a map of verifiers indexed by the signature the verifier corresponds to.
+func verifyEnvelope(allPubKeyBytes [][]byte, env *dsse.Envelope) (map[string]*x509.PublicKey, error) {
+	// generate a fake id for these keys so we can get back to the key bytes and match them to their corresponding signature
+	verifierBySig := make(map[string]*x509.PublicKey)
+	allSigs := make(map[string]struct{})
+	for _, sig := range env.Signatures {
+		allSigs[sig.Sig] = struct{}{}
+	}
+
+	for _, pubKeyBytes := range allPubKeyBytes {
+		key, err := x509.NewPublicKey(bytes.NewReader(pubKeyBytes))
+		if err != nil {
+			return nil, fmt.Errorf("could not parse public key as x509: %w", err)
+		}
+
+		vfr, err := signature.LoadVerifier(key.CryptoPubKey(), crypto.SHA256)
+		if err != nil {
+			return nil, fmt.Errorf("could not load verifier: %w", err)
+		}
+
+		dsseVfr, err := dsse.NewEnvelopeVerifier(&verifier{
+			v: vfr,
+		})
+
+		if err != nil {
+			return nil, fmt.Errorf("could not use public key as a dsse verifier: %w", err)
+		}
+
+		accepted, err := dsseVfr.Verify(env)
+		if err != nil {
+			return nil, fmt.Errorf("could not verify envelope: %w", err)
+		}
+
+		for _, accept := range accepted {
+			delete(allSigs, accept.Sig.Sig)
+			verifierBySig[accept.Sig.Sig] = key
+		}
+	}
+
+	if len(allSigs) > 0 {
+		return nil, errors.New("all signatures must have a key that verifies it")
+	}
+
+	return verifierBySig, nil
+}
diff --git a/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json b/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json
new file mode 100644
index 0000000..0008e46
--- /dev/null
+++ b/pkg/types/intoto/v0.0.2/intoto_v0_0_2_schema.json
@@ -0,0 +1,102 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "$id": "http://rekor.sigstore.dev/types/intoto/intoto_v0_0_2_schema.json",
+    "title": "intoto v0.0.2 Schema",
+    "description": "Schema for intoto object",
+    "type": "object",
+    "properties": {
+        "content": {
+            "type": "object",
+            "properties": {
+                "envelope": {
+                    "description": "dsse envelope",
+                    "type": "object",
+                    "properties": {
+                        "payload": {
+                            "description": "payload of the envelope",
+                            "type": "string",
+                            "format": "byte",
+                            "writeOnly": true
+                        },
+                        "payloadType": {
+                            "description": "type describing the payload",
+                            "type": "string"
+                        },
+                        "signatures": {
+                            "description": "collection of all signatures of the envelope's payload",
+                            "type": "array",
+                            "minItems": 1,
+                            "items": {
+                                "description": "a signature of the envelope's payload along with the public key for the signature",
+                                "type": "object",
+                                "properties": {
+                                    "keyid": {
+                                        "description": "optional id of the key used to create the signature",
+                                        "type": "string"
+                                    },
+                                    "sig": {
+                                        "description": "signature of the payload",
+                                        "type": "string",
+                                        "format": "byte"
+                                    },
+                                    "publicKey": {
+                                        "description": "public key that corresponds to this signature",
+                                        "type": "string",
+                                        "format": "byte",
+                                        "readOnly": true
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "required": ["payloadType", "signatures"]
+                },
+                "hash": {
+                    "description": "Specifies the hash algorithm and value encompassing the entire signed envelope",
+                    "type": "object",
+                    "properties": {
+                        "algorithm": {
+                            "description": "The hashing function used to compute the hash value",
+                            "type": "string",
+                            "enum": [
+                                "sha256"
+                            ]
+                        },
+                        "value": {
+                            "description": "The hash value for the archive",
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "algorithm",
+                        "value"
+                    ],
+                    "readOnly": true
+                },
+                "payloadHash": {
+                    "description": "Specifies the hash algorithm and value covering the payload within the DSSE envelope",
+                    "type": "object",
+                    "properties": {
+                        "algorithm": {
+                            "description": "The hashing function used to compute the hash value",
+                            "type": "string",
+                            "enum": [ "sha256" ]
+                        },
+                        "value": {
+                            "description": "The hash value of the payload",
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "algorithm",
+                        "value"
+                    ],
+                    "readOnly": true
+                }
+            }
+        }
+    },
+    "required": [
+        "content"
+    ]
+}
diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go
index 4c566a0..95feb3b 100644
--- a/pkg/types/rekord/v0.0.1/entry.go
+++ b/pkg/types/rekord/v0.0.1/entry.go
@@ -391,19 +391,21 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types
 
 	re.RekordObj.Signature.PublicKey = &models.RekordV001SchemaSignaturePublicKey{}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		if props.PublicKeyPath == nil {
-			return nil, errors.New("public key must be provided to verify detached signature")
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided to verify detached signature")
 		}
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
-		re.RekordObj.Signature.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
-	} else {
-		re.RekordObj.Signature.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
 
+	re.RekordObj.Signature.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0])
+
 	if err := re.validate(); err != nil {
 		return nil, err
 	}
diff --git a/pkg/types/rpm/v0.0.1/entry.go b/pkg/types/rpm/v0.0.1/entry.go
index b8b00a8..1523a42 100644
--- a/pkg/types/rpm/v0.0.1/entry.go
+++ b/pkg/types/rpm/v0.0.1/entry.go
@@ -342,19 +342,21 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types
 
 	re.RPMModel.PublicKey = &models.RpmV001SchemaPublicKey{}
 	publicKeyBytes := props.PublicKeyBytes
-	if publicKeyBytes == nil {
-		if props.PublicKeyPath == nil {
-			return nil, errors.New("public key must be provided to verify RPM signature")
+	if len(publicKeyBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one public key must be provided to verify RPM signature")
 		}
-		publicKeyBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading public key file: %w", err)
 		}
-		re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
-	} else {
-		re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes)
+		publicKeyBytes = append(publicKeyBytes, keyBytes)
+	} else if len(publicKeyBytes) != 1 {
+		return nil, errors.New("only one public key must be provided")
 	}
 
+	re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0])
+
 	if err := re.validate(); err != nil {
 		return nil, err
 	}
diff --git a/pkg/types/tuf/v0.0.1/entry.go b/pkg/types/tuf/v0.0.1/entry.go
index e53e069..8c3a9ff 100644
--- a/pkg/types/tuf/v0.0.1/entry.go
+++ b/pkg/types/tuf/v0.0.1/entry.go
@@ -333,26 +333,25 @@ func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types
 
 	rootBytes := props.PublicKeyBytes
 	re.TufObj.Root = &models.TUFV001SchemaRoot{}
-	if rootBytes == nil {
-		if props.PublicKeyPath == nil {
-			return nil, errors.New("path to root file must be specified")
+	if len(rootBytes) == 0 {
+		if len(props.PublicKeyPaths) != 1 {
+			return nil, errors.New("only one path to root file must be specified")
 		}
-		rootBytes, err = ioutil.ReadFile(filepath.Clean(props.PublicKeyPath.Path))
+		keyBytes, err := ioutil.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
 		if err != nil {
 			return nil, fmt.Errorf("error reading root file: %w", err)
 		}
-		s := &data.Signed{}
-		if err := json.Unmarshal(rootBytes, s); err != nil {
-			return nil, err
-		}
-		re.TufObj.Root.Content = s
-	} else {
-		s := &data.Signed{}
-		if err := json.Unmarshal(rootBytes, s); err != nil {
-			return nil, err
-		}
-		re.TufObj.Root.Content = s
+		rootBytes = append(rootBytes, keyBytes)
+
+	} else if len(rootBytes) != 1 {
+		return nil, errors.New("only one root key must be provided")
+	}
+
+	root := &data.Signed{}
+	if err := json.Unmarshal(rootBytes[0], root); err != nil {
+		return nil, err
 	}
+	re.TufObj.Root.Content = root
 
 	if err := re.Validate(); err != nil {
 		return nil, err


Test patch:
diff --git a/cmd/rekor-cli/app/pflags_test.go b/cmd/rekor-cli/app/pflags_test.go
index 64887c4..c48efc3 100644
--- a/cmd/rekor-cli/app/pflags_test.go
+++ b/cmd/rekor-cli/app/pflags_test.go
@@ -37,6 +37,7 @@ func TestArtifactPFlags(t *testing.T) {
 		artifact              string
 		signature             string
 		publicKey             string
+		multiPublicKey        []string
 		uuid                  string
 		aad                   string
 		uuidRequired          bool
@@ -373,6 +374,22 @@ func TestArtifactPFlags(t *testing.T) {
 			expectParseSuccess:    true,
 			expectValidateSuccess: false,
 		},
+		{
+			caseDesc:              "valid intoto - one keys",
+			typeStr:               "intoto",
+			artifact:              "../../../tests/intoto_dsse.json",
+			publicKey:             "../../../tests/intoto_dsse.pem",
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
+		{
+			caseDesc:              "valid intoto - multi keys",
+			typeStr:               "intoto",
+			artifact:              "../../../tests/intoto_multi_dsse.json",
+			multiPublicKey:        []string{"../../../tests/intoto_dsse.pem", "../../../tests/intoto_multi_pub2.pem"},
+			expectParseSuccess:    true,
+			expectValidateSuccess: true,
+		},
 	}
 
 	for _, tc := range tests {
@@ -405,6 +422,11 @@ func TestArtifactPFlags(t *testing.T) {
 		if tc.publicKey != "" {
 			args = append(args, "--public-key", tc.publicKey)
 		}
+		if len(tc.multiPublicKey) > 0 {
+			for _, key := range tc.multiPublicKey {
+				args = append(args, "--public-key", key)
+			}
+		}
 		if tc.uuid != "" {
 			args = append(args, "--uuid", tc.uuid)
 		}
@@ -740,6 +762,11 @@ func TestParseTypeFlag(t *testing.T) {
 		{
 			caseDesc:      "explicit intoto v0.0.1",
 			typeStr:       "intoto:0.0.1",
+			expectSuccess: false,
+		},
+		{
+			caseDesc:      "explicit intoto v0.0.2",
+			typeStr:       "intoto:0.0.2",
 			expectSuccess: true,
 		},
 		{
diff --git a/pkg/types/intoto/v0.0.2/entry_test.go b/pkg/types/intoto/v0.0.2/entry_test.go
new file mode 100644
index 0000000..66bd21a
--- /dev/null
+++ b/pkg/types/intoto/v0.0.2/entry_test.go
@@ -0,0 +1,470 @@
+//
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package intoto
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"reflect"
+	"sort"
+	"strings"
+	"testing"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/in-toto/in-toto-golang/in_toto"
+	slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
+	"github.com/secure-systems-lab/go-securesystemslib/dsse"
+	"github.com/sigstore/rekor/pkg/generated/models"
+	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"go.uber.org/goleak"
+)
+
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
+
+func TestNewEntryReturnType(t *testing.T) {
+	entry := NewEntry()
+	if reflect.TypeOf(entry) != reflect.ValueOf(&V002Entry{}).Type() {
+		t.Errorf("invalid type returned from NewEntry: %T", entry)
+	}
+}
+
+func envelope(t *testing.T, k *ecdsa.PrivateKey, payload []byte) *dsse.Envelope {
+
+	s, err := signature.LoadECDSASigner(k, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	signer, err := in_toto.NewDSSESigner(
+		&verifier{
+			s: s,
+		})
+	if err != nil {
+		t.Fatal(err)
+	}
+	dsseEnv, err := signer.SignPayload(payload)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return dsseEnv
+}
+
+func multiSignEnvelope(t *testing.T, k []*ecdsa.PrivateKey, payload []byte) *dsse.Envelope {
+	evps := []*verifier{}
+	for _, key := range k {
+		s, err := signature.LoadECDSASigner(key, crypto.SHA256)
+		if err != nil {
+			t.Fatal(err)
+		}
+		evps = append(evps, &verifier{
+			s: s,
+		})
+	}
+
+	signer, err := dsse.NewMultiEnvelopeSigner(2, evps[0], evps[1])
+	if err != nil {
+		t.Fatal(err)
+	}
+	dsseEnv, err := signer.SignPayload(in_toto.PayloadType, payload)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return dsseEnv
+}
+
+func createRekorEnvelope(dsseEnv *dsse.Envelope, pub [][]byte) *models.IntotoV002SchemaContentEnvelope {
+
+	env := &models.IntotoV002SchemaContentEnvelope{}
+	b64 := strfmt.Base64([]byte(dsseEnv.Payload))
+	env.Payload = b64
+	env.PayloadType = &dsseEnv.PayloadType
+
+	for i, sig := range dsseEnv.Signatures {
+		env.Signatures = append(env.Signatures, &models.IntotoV002SchemaContentEnvelopeSignaturesItems0{
+			Keyid:     sig.KeyID,
+			Sig:       strfmt.Base64([]byte(sig.Sig)),
+			PublicKey: strfmt.Base64(pub[i]),
+		})
+	}
+	return env
+}
+
+func envelopeHash(t *testing.T, dsseEnv *dsse.Envelope) string {
+	val, err := json.Marshal(dsseEnv)
+	if err != nil {
+		t.Fatal(err)
+	}
+	h := sha256.Sum256(val)
+	return hex.EncodeToString(h[:])
+}
+
+func TestV002Entry_Unmarshal(t *testing.T) {
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pub := pem.EncodeToMemory(&pem.Block{
+		Bytes: der,
+		Type:  "PUBLIC KEY",
+	})
+
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	ca := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+	}
+	caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pemBytes := pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: caBytes,
+	})
+
+	invalid := dsse.Envelope{
+		Payload: "hello",
+		Signatures: []dsse.Signature{
+			{
+				Sig: string(strfmt.Base64("foobar")),
+			},
+		},
+	}
+
+	validPayload := "hellothispayloadisvalid"
+
+	tests := []struct {
+		env     *dsse.Envelope
+		name    string
+		it      *models.IntotoV002Schema
+		wantErr bool
+	}{
+		{
+			name:    "empty",
+			it:      &models.IntotoV002Schema{},
+			wantErr: true,
+		},
+		{
+			name: "missing envelope",
+			it: &models.IntotoV002Schema{
+				Content: &models.IntotoV002SchemaContent{
+					Hash: &models.IntotoV002SchemaContentHash{
+						Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256),
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			env:  envelope(t, key, []byte(validPayload)),
+			name: "valid",
+			it: &models.IntotoV002Schema{
+				Content: &models.IntotoV002SchemaContent{
+					Envelope: createRekorEnvelope(envelope(t, key, []byte(validPayload)), [][]byte{pub}),
+					Hash: &models.IntotoV002SchemaContentHash{
+						Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256),
+						Value:     swag.String(envelopeHash(t, envelope(t, key, []byte(validPayload)))),
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			env:  envelope(t, priv, []byte(validPayload)),
+			name: "cert",
+			it: &models.IntotoV002Schema{
+				Content: &models.IntotoV002SchemaContent{
+					Envelope: createRekorEnvelope(envelope(t, priv, []byte(validPayload)), [][]byte{pemBytes}),
+					Hash: &models.IntotoV002SchemaContentHash{
+						Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256),
+						Value:     swag.String(envelopeHash(t, envelope(t, priv, []byte(validPayload)))),
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			env:  &invalid,
+			name: "invalid",
+			it: &models.IntotoV002Schema{
+				Content: &models.IntotoV002SchemaContent{
+					Envelope: createRekorEnvelope(&invalid, [][]byte{pub}),
+					Hash: &models.IntotoV002SchemaContentHash{
+						Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256),
+						Value:     swag.String(envelopeHash(t, &invalid)),
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			env:  envelope(t, key, []byte(validPayload)),
+			name: "invalid key",
+			it: &models.IntotoV002Schema{
+				Content: &models.IntotoV002SchemaContent{
+					Envelope: createRekorEnvelope(envelope(t, key, []byte(validPayload)), [][]byte{[]byte("notavalidkey")}),
+					Hash: &models.IntotoV002SchemaContentHash{
+						Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256),
+						Value:     swag.String(envelopeHash(t, envelope(t, key, []byte(validPayload)))),
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			env:  multiSignEnvelope(t, []*ecdsa.PrivateKey{key, priv}, []byte(validPayload)),
+			name: "multi-key",
+			it: &models.IntotoV002Schema{
+				Content: &models.IntotoV002SchemaContent{
+					Envelope: createRekorEnvelope(multiSignEnvelope(t, []*ecdsa.PrivateKey{key, priv}, []byte(validPayload)), [][]byte{pub, pemBytes}),
+					Hash: &models.IntotoV002SchemaContentHash{
+						Algorithm: swag.String(models.IntotoV002SchemaContentHashAlgorithmSha256),
+						Value:     swag.String(envelopeHash(t, multiSignEnvelope(t, []*ecdsa.PrivateKey{key, priv}, []byte(validPayload)))),
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			v := &V002Entry{}
+
+			it := &models.Intoto{
+				Spec: tt.it,
+			}
+
+			var uv = func() error {
+				if err := v.Unmarshal(it); err != nil {
+					return err
+				}
+				want := []string{}
+				for _, sig := range v.IntotoObj.Content.Envelope.Signatures {
+					keyHash := sha256.Sum256(sig.PublicKey)
+					want = append(want, "sha256:"+hex.EncodeToString(keyHash[:]))
+				}
+				decodedPayload, err := base64.StdEncoding.DecodeString(tt.env.Payload)
+				if err != nil {
+					return fmt.Errorf("could not decode envelope payload: %w", err)
+				}
+				h := sha256.Sum256(decodedPayload)
+				want = append(want, "sha256:"+hex.EncodeToString(h[:]))
+
+				if !reflect.DeepEqual(v.AttestationKey(), "sha256:"+hex.EncodeToString(h[:])) {
+					t.Errorf("V002Entry.AttestationKey() = %v, want %v", v.AttestationKey(), "sha256:"+hex.EncodeToString(h[:]))
+				}
+
+				hashkey := strings.ToLower(fmt.Sprintf("%s:%s", *tt.it.Content.Hash.Algorithm, *tt.it.Content.Hash.Value))
+				want = append(want, hashkey)
+				got, _ := v.IndexKeys()
+				sort.Strings(got)
+				sort.Strings(want)
+				if !reflect.DeepEqual(got, want) {
+					t.Errorf("V002Entry.IndexKeys() = %v, want %v", got, want)
+				}
+				payloadBytes, _ := v.env.DecodeB64Payload()
+				payloadSha := sha256.Sum256(payloadBytes)
+				payloadHash := hex.EncodeToString(payloadSha[:])
+
+				canonicalBytes, err := v.Canonicalize(context.Background())
+				if err != nil {
+					t.Errorf("error canonicalizing entry: %v", err)
+				}
+
+				pe, err := models.UnmarshalProposedEntry(bytes.NewReader(canonicalBytes), runtime.JSONConsumer())
+				if err != nil {
+					t.Errorf("unexpected err from Unmarshalling canonicalized entry for '%v': %v", tt.name, err)
+				}
+				canonicalEntry, err := types.UnmarshalEntry(pe)
+				if err != nil {
+					t.Errorf("unexpected err from type-specific unmarshalling for '%v': %v", tt.name, err)
+				}
+				canonicalV002 := canonicalEntry.(*V002Entry)
+				fmt.Printf("%v", canonicalV002.IntotoObj.Content)
+				if *canonicalV002.IntotoObj.Content.Hash.Value != *tt.it.Content.Hash.Value {
+					t.Errorf("envelope hashes do not match post canonicalization: %v %v", *canonicalV002.IntotoObj.Content.Hash.Value, *tt.it.Content.Hash.Value)
+				}
+				if canonicalV002.AttestationKey() != "" && *canonicalV002.IntotoObj.Content.PayloadHash.Value != payloadHash {
+					t.Errorf("payload hashes do not match post canonicalization: %v %v", canonicalV002.IntotoObj.Content.PayloadHash.Value, payloadHash)
+				}
+				canonicalIndexKeys, _ := canonicalV002.IndexKeys()
+				if !cmp.Equal(got, canonicalIndexKeys, cmpopts.SortSlices(func(x, y string) bool { return x < y })) {
+					t.Errorf("index keys from hydrated object do not match those generated from canonicalized (and re-hydrated) object: %v %v", got, canonicalIndexKeys)
+				}
+
+				return nil
+			}
+			if err := uv(); (err != nil) != tt.wantErr {
+				t.Errorf("V002Entry.Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestV002Entry_IndexKeys(t *testing.T) {
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	pub := pem.EncodeToMemory(&pem.Block{
+		Bytes: der,
+		Type:  "PUBLIC KEY",
+	})
+
+	tests := []struct {
+		name      string
+		statement in_toto.Statement
+		want      []string
+	}{
+		{
+			name: "standard",
+			want: []string{},
+			statement: in_toto.Statement{
+				Predicate: "hello",
+			},
+		},
+		{
+			name: "subject",
+			want: []string{"sha256:foo"},
+			statement: in_toto.Statement{
+				StatementHeader: in_toto.StatementHeader{
+					Subject: []in_toto.Subject{
+						{
+							Name: "foo",
+							Digest: map[string]string{
+								"sha256": "foo",
+							},
+						},
+					},
+				},
+				Predicate: "hello",
+			},
+		},
+		{
+			name: "slsa",
+			want: []string{"sha256:bar"},
+			statement: in_toto.Statement{
+				Predicate: slsa.ProvenancePredicate{
+					Materials: []slsa.ProvenanceMaterial{
+						{
+							URI: "foo",
+							Digest: map[string]string{
+								"sha256": "bar",
+							}},
+					},
+				},
+			},
+		},
+		{
+			name: "slsa wit header",
+			want: []string{"sha256:foo", "sha256:bar"},
+			statement: in_toto.Statement{
+				StatementHeader: in_toto.StatementHeader{
+					Subject: []in_toto.Subject{
+						{
+							Name: "foo",
+							Digest: map[string]string{
+								"sha256": "foo",
+							},
+						},
+					},
+				},
+				Predicate: slsa.ProvenancePredicate{
+					Materials: []slsa.ProvenanceMaterial{
+						{
+							URI: "foo",
+							Digest: map[string]string{
+								"sha256": "bar",
+							}},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b, err := json.Marshal(tt.statement)
+			if err != nil {
+				t.Fatal(err)
+			}
+			payloadHash := sha256.Sum256(b)
+			v := V002Entry{
+				IntotoObj: models.IntotoV002Schema{
+					Content: &models.IntotoV002SchemaContent{
+						Envelope: createRekorEnvelope(envelope(t, key, b), [][]byte{pub}),
+						Hash: &models.IntotoV002SchemaContentHash{
+							Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256),
+							Value:     swag.String(envelopeHash(t, envelope(t, key, b))),
+						},
+						PayloadHash: &models.IntotoV002SchemaContentPayloadHash{
+							Algorithm: swag.String(models.IntotoV001SchemaContentHashAlgorithmSha256),
+							Value:     swag.String(hex.EncodeToString(payloadHash[:])),
+						},
+					},
+				},
+				env: *envelope(t, key, b),
+			}
+			want := []string{}
+			for _, sig := range v.IntotoObj.Content.Envelope.Signatures {
+				keyHash := sha256.Sum256(sig.PublicKey)
+				want = append(want, "sha256:"+hex.EncodeToString(keyHash[:]))
+			}
+
+			want = append(want, "sha256:"+hex.EncodeToString(payloadHash[:]))
+
+			hashkey := strings.ToLower("sha256:" + *v.IntotoObj.Content.Hash.Value)
+			want = append(want, hashkey)
+			want = append(want, tt.want...)
+			got, _ := v.IndexKeys()
+			sort.Strings(got)
+			sort.Strings(want)
+			if !cmp.Equal(got, want) {
+				t.Errorf("V001Entry.IndexKeys() = %v, want %v", got, want)
+			}
+		})
+	}
+}
diff --git a/tests/e2e_test.go b/tests/e2e_test.go
index 327c047..4e99d27 100644
--- a/tests/e2e_test.go
+++ b/tests/e2e_test.go
@@ -23,6 +23,7 @@ import (
 	"context"
 	"crypto"
 	"crypto/ecdsa"
+	"crypto/rsa"
 	"crypto/sha256"
 	"crypto/x509"
 	"encoding/base64"
@@ -30,18 +31,22 @@ import (
 	"encoding/json"
 	"encoding/pem"
 	"fmt"
-	"golang.org/x/sync/errgroup"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"reflect"
+	"runtime"
 	"strconv"
 	"strings"
 	"testing"
 	"time"
 
+	"golang.org/x/sync/errgroup"
+	"sigs.k8s.io/release-utils/version"
+
 	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
 	"github.com/go-openapi/strfmt"
 	"github.com/go-openapi/swag"
@@ -55,6 +60,7 @@ import (
 	"github.com/sigstore/rekor/pkg/sharding"
 	"github.com/sigstore/rekor/pkg/signer"
 	"github.com/sigstore/rekor/pkg/types"
+	_ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
 	rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
 	"github.com/sigstore/rekor/pkg/util"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
@@ -495,14 +501,20 @@ func TestIntoto(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	signer, err := dsse.NewEnvelopeSigner(&IntotoSigner{
-		priv: priv.(*ecdsa.PrivateKey),
+
+	s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	signer, err := dsse.NewEnvelopeSigner(&verifier{
+		s: s,
 	})
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	env, err := signer.SignPayload("application/vnd.in-toto+json", b)
+	env, err := signer.SignPayload(in_toto.PayloadType, b)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -515,6 +527,10 @@ func TestIntoto(t *testing.T) {
 	write(t, string(eb), attestationPath)
 	write(t, ecdsaPub, pubKeyPath)
 
+	// ensure that we can't upload a intoto v0.0.1 entry
+	v001out := runCliErr(t, "upload", "--artifact", attestationPath, "--type", "intoto:0.0.1", "--public-key", pubKeyPath)
+	outputContains(t, v001out, "type intoto does not support version 0.0.1")
+
 	// If we do it twice, it should already exist
 	out := runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
 	outputContains(t, out, "Created entry at")
@@ -537,7 +553,7 @@ func TestIntoto(t *testing.T) {
 
 	attHash := sha256.Sum256(b)
 
-	intotoModel := &models.IntotoV001Schema{}
+	intotoModel := &models.IntotoV002Schema{}
 	if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil {
 		t.Errorf("could not convert body into intoto type: %v", err)
 	}
@@ -559,6 +575,237 @@ func TestIntoto(t *testing.T) {
 
 }
 
+func TestIntotoMultiSig(t *testing.T) {
+	td := t.TempDir()
+	attestationPath := filepath.Join(td, "attestation.json")
+	ecdsapubKeyPath := filepath.Join(td, "ecdsapub.pem")
+	rsapubKeyPath := filepath.Join(td, "rsapub.pem")
+
+	// Get some random data so it's unique each run
+	d := randomData(t, 10)
+	id := base64.StdEncoding.EncodeToString(d)
+
+	it := in_toto.ProvenanceStatement{
+		StatementHeader: in_toto.StatementHeader{
+			Type:          in_toto.StatementInTotoV01,
+			PredicateType: slsa.PredicateSLSAProvenance,
+			Subject: []in_toto.Subject{
+				{
+					Name: "foobar",
+					Digest: slsa.DigestSet{
+						"foo": "bar",
+					},
+				},
+			},
+		},
+		Predicate: slsa.ProvenancePredicate{
+			Builder: slsa.ProvenanceBuilder{
+				ID: "foo" + id,
+			},
+		},
+	}
+
+	b, err := json.Marshal(it)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	evps := []*verifier{}
+
+	pb, _ := pem.Decode([]byte(ecdsaPriv))
+	priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	signECDSA, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	evps = append(evps, &verifier{
+		s: signECDSA,
+	})
+
+	pbRSA, _ := pem.Decode([]byte(rsaKey))
+	rsaPriv, err := x509.ParsePKCS8PrivateKey(pbRSA.Bytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	signRSA, err := signature.LoadRSAPKCS1v15Signer(rsaPriv.(*rsa.PrivateKey), crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	evps = append(evps, &verifier{
+		s: signRSA,
+	})
+
+	signer, err := dsse.NewMultiEnvelopeSigner(2, evps[0], evps[1])
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	env, err := signer.SignPayload(in_toto.PayloadType, b)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	eb, err := json.Marshal(env)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	write(t, string(eb), attestationPath)
+	write(t, ecdsaPub, ecdsapubKeyPath)
+	write(t, pubKey, rsapubKeyPath)
+
+	// ensure that we can't upload a intoto v0.0.1 entry
+	v001out := runCliErr(t, "upload", "--artifact", attestationPath, "--type", "intoto:0.0.1", "--public-key", ecdsapubKeyPath, "--public-key", rsapubKeyPath)
+	outputContains(t, v001out, "type intoto does not support version 0.0.1")
+
+	// If we do it twice, it should already exist
+	out := runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", ecdsapubKeyPath, "--public-key", rsapubKeyPath)
+	outputContains(t, out, "Created entry at")
+	uuid := getUUIDFromUploadOutput(t, out)
+
+	out = runCli(t, "get", "--uuid", uuid, "--format=json")
+	g := getOut{}
+	if err := json.Unmarshal([]byte(out), &g); err != nil {
+		t.Fatal(err)
+	}
+	// The attestation should be stored at /var/run/attestations/$uuid
+
+	got := in_toto.ProvenanceStatement{}
+	if err := json.Unmarshal([]byte(g.Attestation), &got); err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(it, got); diff != "" {
+		t.Errorf("diff: %s", diff)
+	}
+
+	attHash := sha256.Sum256([]byte(g.Attestation))
+
+	intotoV002Model := &models.IntotoV002Schema{}
+	if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoV002Model); err != nil {
+		t.Errorf("could not convert body into intoto type: %v", err)
+	}
+	if intotoV002Model.Content.Hash == nil {
+		t.Errorf("could not find hash over attestation %v", intotoV002Model)
+	}
+	recordedPayloadHash, err := hex.DecodeString(*intotoV002Model.Content.PayloadHash.Value)
+	if err != nil {
+		t.Errorf("error converting attestation hash to []byte: %v", err)
+	}
+
+	if !bytes.Equal(attHash[:], recordedPayloadHash) {
+		t.Fatal(fmt.Errorf("attestation hash %v doesnt match the payload we sent %v", hex.EncodeToString(attHash[:]),
+			*intotoV002Model.Content.PayloadHash.Value))
+	}
+
+	out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", ecdsapubKeyPath, "--public-key", rsapubKeyPath)
+	outputContains(t, out, "Entry already exists")
+
+}
+
+func TestIntotoBlockV001(t *testing.T) {
+	td := t.TempDir()
+	attestationPath := filepath.Join(td, "attestation.json")
+	pubKeyPath := filepath.Join(td, "pub.pem")
+
+	// Get some random data so it's unique each run
+	d := randomData(t, 10)
+	id := base64.StdEncoding.EncodeToString(d)
+
+	it := in_toto.ProvenanceStatement{
+		StatementHeader: in_toto.StatementHeader{
+			Type:          in_toto.StatementInTotoV01,
+			PredicateType: slsa.PredicateSLSAProvenance,
+			Subject: []in_toto.Subject{
+				{
+					Name: "foobar",
+					Digest: slsa.DigestSet{
+						"foo": "bar",
+					},
+				},
+			},
+		},
+		Predicate: slsa.ProvenancePredicate{
+			Builder: slsa.ProvenanceBuilder{
+				ID: "foo" + id,
+			},
+		},
+	}
+
+	b, err := json.Marshal(it)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	pb, _ := pem.Decode([]byte(ecdsaPriv))
+	priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	signer, err := dsse.NewEnvelopeSigner(&verifier{
+		s: s,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	env, err := signer.SignPayload(in_toto.PayloadType, b)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	eb, err := json.Marshal(env)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	uaString := fmt.Sprintf("rekor-cli/%s (%s; %s)", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH)
+
+	write(t, string(eb), attestationPath)
+	write(t, ecdsaPub, pubKeyPath)
+
+	rekorClient, err := client.GetRekorClient("http://localhost:3000", client.WithUserAgent(uaString))
+	if err != nil {
+		t.Fatal(err)
+	}
+	var entry models.ProposedEntry
+	params := entries.NewCreateLogEntryParams()
+	params.SetTimeout(time.Duration(30) * time.Second)
+
+	props := &types.ArtifactProperties{}
+
+	props.ArtifactPath = &url.URL{Path: attestationPath}
+
+	collectedKeys := []*url.URL{{Path: pubKeyPath}}
+	props.PublicKeyPaths = collectedKeys
+
+	entry, err = types.NewProposedEntry(context.Background(), "intoto", "0.0.1", *props)
+	if err != nil {
+		t.Fatal(err)
+	}
+	params.SetProposedEntry(entry)
+
+	_, err = rekorClient.Entries.CreateLogEntry(params)
+	if err == nil {
+		t.Fatal("insertion of v0.0.1 entry should fail")
+	}
+	if !strings.Contains(err.Error(), "entry kind 'intoto' does not support inserting entries of version '0.0.1'") {
+		t.Errorf("Expected error as intoto v0.0.1 should not be allowed to be entered into rekor")
+	}
+}
+
 func TestTimestampArtifact(t *testing.T) {
 	var out string
 	out = runCli(t, "upload", "--type", "rfc3161", "--artifact", "test.tsr")
diff --git a/tests/harness_test.go b/tests/harness_test.go
index 3286cba..88441c4 100644
--- a/tests/harness_test.go
+++ b/tests/harness_test.go
@@ -19,6 +19,7 @@ package e2e
 
 import (
 	"bytes"
+	"crypto"
 	"crypto/ecdsa"
 	"crypto/sha256"
 	"crypto/x509"
@@ -42,6 +43,7 @@ import (
 	"github.com/sigstore/rekor/pkg/generated/models"
 	"github.com/sigstore/rekor/pkg/sharding"
 	"github.com/sigstore/rekor/pkg/types"
+	"github.com/sigstore/sigstore/pkg/signature"
 )
 
 type StoredEntry struct {
@@ -121,8 +123,14 @@ func TestHarnessAddIntoto(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	signer, err := dsse.NewEnvelopeSigner(&IntotoSigner{
-		priv: priv.(*ecdsa.PrivateKey),
+
+	s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	signer, err := dsse.NewEnvelopeSigner(&verifier{
+		s: s,
 	})
 	if err != nil {
 		t.Fatal(err)
@@ -164,7 +172,7 @@ func TestHarnessAddIntoto(t *testing.T) {
 
 	attHash := sha256.Sum256(b)
 
-	intotoModel := &models.IntotoV001Schema{}
+	intotoModel := &models.IntotoV002Schema{}
 	if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil {
 		t.Errorf("could not convert body into intoto type: %v", err)
 	}
diff --git a/tests/intoto_multi_dsse.json b/tests/intoto_multi_dsse.json
new file mode 100644
index 0000000..7e24511
--- /dev/null
+++ b/tests/intoto_multi_dsse.json
@@ -0,0 +1,6 @@
+{"payloadType":"application/vnd.in-toto+json",
+"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiJmb29iYXIiLCJkaWdlc3QiOnsiZm9vIjoiYmFyIn19XSwicHJlZGljYXRlIjp7ImJ1aWxkZXIiOnsiaWQiOiJmb29BNi9QWW1CNmNCQXRZQT09In0sImJ1aWxkVHlwZSI6IiIsImludm9jYXRpb24iOnsiY29uZmlnU291cmNlIjp7fX19fQ==",
+"signatures":[{
+    "keyid":"","sig":"MEUCIQC4/1MwOahpGT/oh6DZFuwZiG9yXpfo22wh+7mGn9lQ4gIgIL2LFt0lxYbFngf4ze/FNvkybYzwtmkX2XrcnGMpOqo="},
+    {"keyid":"","sig":"PhNAdsvKEVqv+cF/QvjEOz+fTtoNWQfa9gCWnOpm5yWVFRiu6he5jvw6A8ESRXxV3KnEcyBFCfCbITNK2fpXQEOc0gNDcsil1m/Pzv/JonhtH/TwIjdJ8zy4WEUVHZMfj5IIeibp3U2ACvmCc9HEUPCc6VM0hq7ri/VnKLcCCGbKmxwHrVXArv1hKBrcP7s52tuxCsVr5+3Z7eKPx5WkycOhpUhSVhMHjOCj9e3mveiw4dYuwdrgQHtqZJhUg0WFlVCQTQdcLxIII1g7BudA38yzfOgkbwgfoZ2Dh3iF7uRHIUdFxxpqF2oQeU2uDv3dT9cCfRd/kDtU6hbuVgixgw=="}]
+}
diff --git a/tests/intoto_multi_pub2.pem b/tests/intoto_multi_pub2.pem
new file mode 100644
index 0000000..3135806
--- /dev/null
+++ b/tests/intoto_multi_pub2.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3wqI/TysUiKTgY1bz+wd
+JfEOil4MEsRASKGzJddZ6x9hb+rn2UVoJmuxN62XI0TMoMn4mukgfCgY6jgTB58V
++/LaeSA8Wz1p4gOxhk1mcgbF4HyxR+xlRgYfH4iSbXy+Ez/8ZjM2OO68fKr4JZEA
+5LXZkhJr32JqH+UiFw/wgSPWA8aV0AfRAXHdekJ48B1ChxJTrOJWSPTnj/E0lfLV
+srJKtXDuC8T0vFmVU726tI6fODsEE6VrSahvw1ENUHzI34sbfrmrggwPO4iMAQvq
+wu2gn2lx6ajWsh806FItiXN+DuizMnx4KMBI0IJynoQpWOFbstGiV0LygZkQ6soz
+vwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tests/x509.go b/tests/x509.go
index 0ffefba..81914b9 100644
--- a/tests/x509.go
+++ b/tests/x509.go
@@ -19,8 +19,8 @@
 package e2e
 
 import (
+	"bytes"
 	"crypto"
-	"crypto/ecdsa"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha256"
@@ -30,7 +30,8 @@ import (
 	"io/ioutil"
 	"testing"
 
-	"github.com/secure-systems-lab/go-securesystemslib/dsse"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"github.com/sigstore/sigstore/pkg/signature/options"
 )
 
 // Generated with:
@@ -156,34 +157,33 @@ func createdX509SignedArtifact(t *testing.T, artifactPath, sigPath string) {
 	}
 }
 
-type IntotoSigner struct {
-	priv *ecdsa.PrivateKey
+type verifier struct {
+	s signature.Signer
+	v signature.Verifier
 }
 
-var _ dsse.SignVerifier = &IntotoSigner{}
+func (v *verifier) KeyID() (string, error) {
+	return "", nil
+}
 
-func (it *IntotoSigner) Sign(data []byte) ([]byte, error) {
-	h := sha256.Sum256(data)
-	sig, err := it.priv.Sign(rand.Reader, h[:], crypto.SHA256)
+func (v *verifier) Public() crypto.PublicKey {
+	return v.v.PublicKey
+}
+
+func (v *verifier) Sign(data []byte) (sig []byte, err error) {
+	if v.s == nil {
+		return nil, errors.New("nil signer")
+	}
+	sig, err = v.s.SignMessage(bytes.NewReader(data), options.WithCryptoSignerOpts(crypto.SHA256))
 	if err != nil {
 		return nil, err
 	}
 	return sig, nil
 }
 
-func (it *IntotoSigner) KeyID() (string, error) {
-	return "", nil
-}
-
-func (it *IntotoSigner) Public() crypto.PublicKey {
-	return it.priv.Public()
-}
-
-func (it *IntotoSigner) Verify(data, sig []byte) error {
-	h := sha256.Sum256(data)
-	ok := ecdsa.VerifyASN1(&it.priv.PublicKey, h[:], sig)
-	if ok {
-		return nil
+func (v *verifier) Verify(data, sig []byte) error {
+	if v.v == nil {
+		return errors.New("nil verifier")
 	}
-	return errors.New("invalid signature")
+	return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
 }