Repository: sigstore/rekor-tiles
Pull request #146: Verify entry signatures
Author: login: cmurphy
Created: 2025-03-25T22:12:46Z | Merged: 2025-03-28T17:46:39Z

This copies in in parts of github.com/sigstore/rekor/pkg/pki to help with parsing the public key and checking the signature.

Fixes https://github.com/sigstore/rekor-tiles/issues/10

<!--
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 sufficient time for discussions to take place. Please 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?
-->

#### 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.

-->

#### 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

-->



Diff:
diff --git a/go.mod b/go.mod
index ec8f90a7..1b43b97c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,13 @@ module github.com/sigstore/rekor-tiles
 go 1.24.0
 
 require (
+	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
 	github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
 	github.com/prometheus/client_golang v1.21.1
 	github.com/sigstore/protobuf-specs v0.4.0
+	github.com/sigstore/rekor v1.3.9
 	github.com/sigstore/sigstore v1.9.2-0.20250311183026-c049f8ddc600
 	github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.2-0.20250311183026-c049f8ddc600
 	github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.2-0.20250311183026-c049f8ddc600
@@ -143,8 +145,7 @@ require (
 	go.opentelemetry.io/otel/sdk v1.34.0 // indirect
 	go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
 	go.opentelemetry.io/otel/trace v1.34.0 // indirect
-	go.uber.org/atomic v1.9.0 // indirect
-	go.uber.org/multierr v1.9.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/crypto v0.36.0 // indirect
 	golang.org/x/net v0.37.0 // indirect
 	golang.org/x/oauth2 v0.28.0 // indirect
diff --git a/go.sum b/go.sum
index 9829f495..016c0935 100644
--- a/go.sum
+++ b/go.sum
@@ -663,6 +663,8 @@ github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0I
 github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
 github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
 github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
 github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
@@ -1093,6 +1095,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3
 github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw=
 github.com/sigstore/protobuf-specs v0.4.0 h1:yoZbdh0kZYKOSiVbYyA8J3f2wLh5aUk2SQB7LgAfIdU=
 github.com/sigstore/protobuf-specs v0.4.0/go.mod h1:FKW5NYhnnFQ/Vb9RKtQk91iYd0MKJ9AxyqInEwU6+OI=
+github.com/sigstore/rekor v1.3.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU=
+github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM=
 github.com/sigstore/sigstore v1.9.2-0.20250311183026-c049f8ddc600 h1:JHkNzxxB5ZukIt+1uIMvB695xOtQ334htdelYMPLnvY=
 github.com/sigstore/sigstore v1.9.2-0.20250311183026-c049f8ddc600/go.mod h1:hRFC8Rc3D7khJdppxtMGIFCNDt+dINy4AhsWgR9Wt1c=
 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.2-0.20250311183026-c049f8ddc600 h1:VSh0XhMa+21pmhwaImF957/TFDDXHLysoyOrl2xpUe8=
@@ -1123,7 +1127,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -1193,12 +1196,10 @@ go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.step.sm/crypto v0.59.1 h1:jUL+5p19YS9YJKLaPUgkS2OdGm7s0+hwP7AqTFyF9Cg=
 go.step.sm/crypto v0.59.1/go.mod h1:XHavmnzfTyPpQE/n4YokEtjiBzP3LZI9/1O061f5y0o=
-go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
-go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
-go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/pkg/pki/identity/identity.go b/pkg/pki/identity/identity.go
new file mode 100644
index 00000000..89b68859
--- /dev/null
+++ b/pkg/pki/identity/identity.go
@@ -0,0 +1,40 @@
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/pki.go
+
+package identity
+
+type Identity struct {
+	// Types include:
+	// - *rsa.PublicKey
+	// - *ecdsa.PublicKey
+	// - ed25519.PublicKey
+	// - *x509.Certificate
+	// - openpgp.EntityList (golang.org/x/crypto/openpgp)
+	// - *minisign.PublicKey (github.com/jedisct1/go-minisign)
+	// - ssh.PublicKey (golang.org/x/crypto/ssh)
+	Crypto any
+	// Raw key or certificate extracted from Crypto. Values include:
+	// - PKIX ASN.1 DER-encoded public key
+	// - ASN.1 DER-encoded certificate
+	Raw []byte
+	// For keys, certificates, and minisign, hex-encoded SHA-256 digest
+	// of the public key or certificate
+	// For SSH and PGP, Fingerprint is the standard for each ecosystem
+	// For SSH, unpadded base-64 encoded SHA-256 digest of the key
+	// For PGP, hex-encoded SHA-1 digest of a key, which can be either
+	// a primary key or subkey
+	Fingerprint string
+}
diff --git a/pkg/pki/pki.go b/pkg/pki/pki.go
new file mode 100644
index 00000000..d0bc6d74
--- /dev/null
+++ b/pkg/pki/pki.go
@@ -0,0 +1,43 @@
+//
+// Copyright 2025 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.
+
+// Copied with modifications from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/pki.go
+
+package pki
+
+import (
+	"io"
+
+	"github.com/sigstore/rekor-tiles/pkg/pki/identity"
+	sigsig "github.com/sigstore/sigstore/pkg/signature"
+)
+
+// PublicKey Generic object representing a public key (regardless of format & algorithm)
+type PublicKey interface {
+	// CanonicalValue returns the canonicalized representation of the public key.
+	CanonicalValue() ([]byte, error)
+	// Subjects returns the subject URIs of the public key.
+	Subjects() []string
+	// Identities returns a list of typed keys and certificates.
+	Identities() ([]identity.Identity, error)
+}
+
+// Signature Generic object representing a signature (regardless of format & algorithm)
+type Signature interface {
+	// CanonicalValue returns the canonicalized representation of the signature.
+	CanonicalValue() ([]byte, error)
+	// Verify verifies the signature.
+	Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error
+}
diff --git a/pkg/pki/x509/e2e.go b/pkg/pki/x509/e2e.go
new file mode 100644
index 00000000..ba543d5e
--- /dev/null
+++ b/pkg/pki/x509/e2e.go
@@ -0,0 +1,195 @@
+//
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/e2e.go
+
+//go:build e2e
+// +build e2e
+
+package x509
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+	"io/ioutil"
+	"testing"
+
+	"github.com/sigstore/rekor/pkg/util"
+	"github.com/sigstore/sigstore/pkg/signature"
+	"github.com/sigstore/sigstore/pkg/signature/options"
+)
+
+// Generated with:
+// openssl ecparam -genkey -name prime256v1 > ec_private.pem
+// openssl pkcs8 -topk8 -in ec_private.pem  -nocrypt
+const ECDSAPriv = `-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmrLtCpBdXgXLUr7o
+nSUPfo3oXMjmvuwTOjpTulIBKlKhRANCAATH6KSpTFe6uXFmW1qNEFXaO7fWPfZt
+pPZrHZ1cFykidZoURKoYXfkohJ+U/USYy8Sd8b4DMd5xDRZCnlDM0h37
+-----END PRIVATE KEY-----`
+
+// Extracted from above with:
+// openssl ec -in ec_private.pem -pubout
+const ECDSAPub = `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx+ikqUxXurlxZltajRBV2ju31j32
+baT2ax2dXBcpInWaFESqGF35KISflP1EmMvEnfG+AzHecQ0WQp5QzNId+w==
+-----END PUBLIC KEY-----`
+
+// Generated with:
+// openssl req -newkey rsa:2048 -nodes -keyout test.key -x509 -out test.crt
+const RSACert = `-----BEGIN CERTIFICATE-----
+MIIDOjCCAiKgAwIBAgIUEP925shVBKERFCsymdSqESLZFyMwDQYJKoZIhvcNAQEL
+BQAwHzEdMBsGCSqGSIb3DQEJARYOdGVzdEByZWtvci5kZXYwHhcNMjEwNDIxMjAy
+ODAzWhcNMjEwNTIxMjAyODAzWjAfMR0wGwYJKoZIhvcNAQkBFg50ZXN0QHJla29y
+LmRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN8KiP08rFIik4GN
+W8/sHSXxDopeDBLEQEihsyXXWesfYW/q59lFaCZrsTetlyNEzKDJ+JrpIHwoGOo4
+EwefFfvy2nkgPFs9aeIDsYZNZnIGxeB8sUfsZUYGHx+Ikm18vhM//GYzNjjuvHyq
++CWRAOS12ZISa99iah/lIhcP8IEj1gPGldAH0QFx3XpCePAdQocSU6ziVkj054/x
+NJXy1bKySrVw7gvE9LxZlVO9urSOnzg7BBOla0mob8NRDVB8yN+LG365q4IMDzuI
+jAEL6sLtoJ9pcemo1rIfNOhSLYlzfg7oszJ8eCjASNCCcp6EKVjhW7LRoldC8oGZ
+EOrKM78CAwEAAaNuMGwwHQYDVR0OBBYEFGjs8EHKT3x1itwwptJLuQQg/hQcMB8G
+A1UdIwQYMBaAFGjs8EHKT3x1itwwptJLuQQg/hQcMA8GA1UdEwEB/wQFMAMBAf8w
+GQYDVR0RBBIwEIEOdGVzdEByZWtvci5kZXYwDQYJKoZIhvcNAQELBQADggEBAAHE
+bYuePN3XpM7pHoCz6g4uTHu0VrezqJyK1ohysgWJmSJzzazUeISXk0xWnHPk1Zxi
+kzoEuysI8b0P7yodMA8e16zbIOL6QbGe3lNXYqRIg+bl+4OPFGVMX8xHNZmeh0kD
+vX1JVS+y9uyo4/z/pm0JhaSCn85ft/Y5uXMQYn1wFR5DAcJH+iWjNX4fipGxGRE9
+Cy0DjFnYJ3SRY4HPQ0oUSQmyhrwe2DiYzeqtbL2KJBXPcFQKWhkf/fupdYFljvcH
+d9NNfRb0p2oFGG/J0ROg9pEcP1/aZP5k8P2pRdt3y7h1MAtmg2bgEdugZgXwAUmM
+BmU8k2FeTuqV15piPCE=
+-----END CERTIFICATE-----`
+
+const RSAKey = `-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfCoj9PKxSIpOB
+jVvP7B0l8Q6KXgwSxEBIobMl11nrH2Fv6ufZRWgma7E3rZcjRMygyfia6SB8KBjq
+OBMHnxX78tp5IDxbPWniA7GGTWZyBsXgfLFH7GVGBh8fiJJtfL4TP/xmMzY47rx8
+qvglkQDktdmSEmvfYmof5SIXD/CBI9YDxpXQB9EBcd16QnjwHUKHElOs4lZI9OeP
+8TSV8tWyskq1cO4LxPS8WZVTvbq0jp84OwQTpWtJqG/DUQ1QfMjfixt+uauCDA87
+iIwBC+rC7aCfaXHpqNayHzToUi2Jc34O6LMyfHgowEjQgnKehClY4Vuy0aJXQvKB
+mRDqyjO/AgMBAAECggEBAIHOAs3Gis8+WjRSjXVjh882DG1QsJwXZQYgPT+vpiAl
+YjKdNpOHRkbd9ARgXY5kEuccxDd7p7E6MM3XFpQf7M51ltpZfWboRgAIgD+WOiHw
+eSbdytr95C6tj11twTJBH+naGk1sTokxv7aaVdKfIjL49oeBexBFmVe4pW9gkmrE
+1z1y1a0RohqbZ0kprYPWjz5UhsNqbCzgkdDqS7IrcOwVg6zvKYFjHnqIHqaJXVif
+FgIfoNt7tz+12FTHI+6OkKoN3YCJueaxneBhITXm6RLOpQWa9qhdUPbkJ9vQNfph
+Qqke4faaxKY9UDma+GpEHR016AWufZp92pd9wQkDn0kCgYEA7w/ZizAkefHoZhZ8
+Isn/fYu4fdtUaVgrnGUVZobiGxWrHRU9ikbAwR7UwbgRSfppGiJdAMq1lyH2irmb
+4OHU64rjuYSlIqUWHLQHWmqUbLUvlDojH/vdmH/Zn0AbrLZaimC5UCjK3Eb7sAMq
+G0tGeDX2JraQvx7KrbC6peTaaaMCgYEA7tgZBiRCQJ7+mNu+gX9x6OXtjsDCh516
+vToRLkxWc7LAbC9LKsuEHl4e3vy1PY/nyuv12Ng2dBq4WDXozAmVgz0ok7rRlIFp
+w8Yj8o/9KuGZkD/7tw/pLsVc9Q3Wf0ACrnAAh7+3dAvn3yg+WHwXzqWIbrseDPt9
+ILCfUoNDpzUCgYAKFCX8y0PObFd67lm/cbq2xUw66iNN6ay1BEH5t5gSwkAbksis
+ar03pyAbJrJ75vXFZ0t6fBFZ1NG7GYYr3fmHEKz3JlN7+W/MN/7TXgjx6FWgLy9J
+6ul1w3YeU6qXBn0ctmU5ru6WiNuVmRyOWAcZjFTbXvkNRbQPzJKh6dsXdwKBgA1D
+FIihxMf/zBVCxl48bF/JPJqbm3GaTfFp4wBWHsrH1yVqrtrOeCSTh1VMZOfpMK60
+0W7b+pIR1cCYJbgGpDWoVLN3QSHk2bGUM/TJB/60jilTVC/DA2ikbtfwj8N7E2sK
+Lw1amN4ptxNOEcAqC8xepqe3XiDMahNBm2cigMQtAoGBAKwrXvss2BKz+/6poJQU
+A0c7jhMN8M9Y5S2Ockw07lrQeAgfu4q+/8ztm0NeHJbk01IJvJY5Nt7bSgwgNVlo
+j7vR2BMAc9U73Ju9aeTl/L6GqmZyA+Ojhl5gA5DPZYqNiqi93ydgRaI6n4+o3dI7
+5wnr40AmbuKCDvMOvN7nMybL
+-----END PRIVATE KEY-----`
+
+// Extracted from the certificate using:
+// openssl x509 -pubkey -noout -in test.crt
+const PubKey = `-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3wqI/TysUiKTgY1bz+wd
+JfEOil4MEsRASKGzJddZ6x9hb+rn2UVoJmuxN62XI0TMoMn4mukgfCgY6jgTB58V
++/LaeSA8Wz1p4gOxhk1mcgbF4HyxR+xlRgYfH4iSbXy+Ez/8ZjM2OO68fKr4JZEA
+5LXZkhJr32JqH+UiFw/wgSPWA8aV0AfRAXHdekJ48B1ChxJTrOJWSPTnj/E0lfLV
+srJKtXDuC8T0vFmVU726tI6fODsEE6VrSahvw1ENUHzI34sbfrmrggwPO4iMAQvq
+wu2gn2lx6ajWsh806FItiXN+DuizMnx4KMBI0IJynoQpWOFbstGiV0LygZkQ6soz
+vwIDAQAB
+-----END PUBLIC KEY-----`
+
+var (
+	CertPrivateKey *rsa.PrivateKey
+	Certificate    *x509.Certificate
+)
+
+func init() {
+	p, _ := pem.Decode([]byte(RSAKey))
+	priv, err := x509.ParsePKCS8PrivateKey(p.Bytes)
+	if err != nil {
+		panic(err)
+	}
+	cpk, ok := priv.(*rsa.PrivateKey)
+	if !ok {
+		panic("unsuccessful conversion")
+	}
+	CertPrivateKey = cpk
+
+	p, _ = pem.Decode([]byte(RSACert))
+	Certificate, err = x509.ParseCertificate(p.Bytes)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func SignX509Cert(b []byte) ([]byte, error) {
+	dgst := sha256.Sum256(b)
+	signature, err := CertPrivateKey.Sign(rand.Reader, dgst[:], crypto.SHA256)
+	return signature, err
+}
+
+// CreatedX509SignedArtifact gets the test dir setup correctly with some random artifacts and keys.
+func CreatedX509SignedArtifact(t *testing.T, artifactPath, sigPath string) {
+	t.Helper()
+	artifact := util.CreateArtifact(t, artifactPath)
+
+	// Sign it with our key and write that to a file
+	signature, err := SignX509Cert([]byte(artifact))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := ioutil.WriteFile(sigPath, []byte(signature), 0644); err != nil {
+		t.Fatal(err)
+	}
+}
+
+type Verifier struct {
+	S signature.Signer
+	v signature.Verifier
+}
+
+func (v *Verifier) KeyID() (string, error) {
+	return "", nil
+}
+
+func (v *Verifier) Public() crypto.PublicKey {
+	return v.v.PublicKey
+}
+
+func (v *Verifier) Sign(_ context.Context, 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(_ context.Context, data, sig []byte) error {
+	if v.v == nil {
+		return errors.New("nil Verifier")
+	}
+	return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data))
+}
diff --git a/pkg/pki/x509/e2e_test.go b/pkg/pki/x509/e2e_test.go
new file mode 100644
index 00000000..457d6261
--- /dev/null
+++ b/pkg/pki/x509/e2e_test.go
@@ -0,0 +1,78 @@
+//
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/e2e_test.go
+
+//go:build e2e
+// +build e2e
+
+package x509
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/sigstore/rekor/pkg/util"
+)
+
+func TestX509(t *testing.T) {
+	td := t.TempDir()
+	artifactPath := filepath.Join(td, "artifact")
+	sigPath := filepath.Join(td, "signature")
+	certPath := filepath.Join(td, "cert.pem")
+	pubKeyPath := filepath.Join(td, "key.pem")
+
+	CreatedX509SignedArtifact(t, artifactPath, sigPath)
+
+	if err := os.WriteFile(certPath, []byte(RSACert), 0o644); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.WriteFile(pubKeyPath, []byte(PubKey), 0o644); err != nil {
+		t.Fatal(err)
+	}
+
+	// If we do it twice, it should already exist
+	out := util.RunCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
+		"--public-key", certPath, "--pki-format", "x509")
+	util.OutputContains(t, out, "Created entry at")
+	out = util.RunCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
+		"--public-key", certPath, "--pki-format", "x509")
+	util.OutputContains(t, out, "Entry already exists")
+
+	// Now upload with the public key rather than the cert. They should NOT be deduped.
+	out = util.RunCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
+		"--public-key", pubKeyPath, "--pki-format", "x509")
+	util.OutputContains(t, out, "Created entry at")
+
+	// Now let's go the other order to be sure. New artifact, key first then cert.
+	CreatedX509SignedArtifact(t, artifactPath, sigPath)
+
+	out = util.RunCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
+		"--public-key", pubKeyPath, "--pki-format", "x509")
+	util.OutputContains(t, out, "Created entry at")
+	out = util.RunCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
+		"--public-key", pubKeyPath, "--pki-format", "x509")
+	util.OutputContains(t, out, "Entry already exists")
+	// This should NOT already exist
+	out = util.RunCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath,
+		"--public-key", certPath, "--pki-format", "x509")
+	util.OutputContains(t, out, "Created entry at")
+	uuid := util.GetUUIDFromUploadOutput(t, out)
+
+	// Search via email
+	out = util.RunCli(t, "search", "--email", "test@rekor.dev")
+	util.OutputContains(t, out, uuid)
+}
diff --git a/pkg/pki/x509/testdata/ec.pub b/pkg/pki/x509/testdata/ec.pub
new file mode 100644
index 00000000..a23c7014
--- /dev/null
+++ b/pkg/pki/x509/testdata/ec.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIxaAc+TaxYoQU0X9NNUJgWffbn6h
+juoEDPQQn80nX/Eus9I/t00ccNpcSrUw1+IPyGp1p9fgmL0DlBf05BNFNQ==
+-----END PUBLIC KEY-----
diff --git a/pkg/pki/x509/testdata/hello_world.txt b/pkg/pki/x509/testdata/hello_world.txt
new file mode 100644
index 00000000..8ab686ea
--- /dev/null
+++ b/pkg/pki/x509/testdata/hello_world.txt
@@ -0,0 +1 @@
+Hello, World!
diff --git a/pkg/pki/x509/testdata/hello_world.txt.sig b/pkg/pki/x509/testdata/hello_world.txt.sig
new file mode 100644
index 00000000..306a9fc4
Binary files /dev/null and b/pkg/pki/x509/testdata/hello_world.txt.sig differ
diff --git a/pkg/pki/x509/testutils/cert_test_utils.go b/pkg/pki/x509/testutils/cert_test_utils.go
new file mode 100644
index 00000000..47615f1b
--- /dev/null
+++ b/pkg/pki/x509/testutils/cert_test_utils.go
@@ -0,0 +1,217 @@
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/testutils/cert_test_utils.go
+
+package testutils
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"math/big"
+	"net"
+	"net/url"
+	"time"
+)
+
+/*
+To use:
+
+rootCert, rootKey, _ := GenerateRootCa()
+subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey)
+leafCert, _, _ := GenerateLeafCert("subject", "oidc-issuer", subCert, subKey)
+
+roots := x509.NewCertPool()
+subs := x509.NewCertPool()
+roots.AddCert(rootCert)
+subs.AddCert(subCert)
+opts := x509.VerifyOptions{
+	Roots:         roots,
+	Intermediates: subs,
+	KeyUsages: []x509.ExtKeyUsage{
+		x509.ExtKeyUsageCodeSigning,
+	},
+}
+_, err := leafCert.Verify(opts)
+*/
+
+func createCertificate(template *x509.Certificate, parent *x509.Certificate, pub interface{}, priv crypto.Signer) (*x509.Certificate, error) {
+	certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv)
+	if err != nil {
+		return nil, err
+	}
+
+	cert, err := x509.ParseCertificate(certBytes)
+	if err != nil {
+		return nil, err
+	}
+	return cert, nil
+}
+
+func GenerateRootCa() (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	rootTemplate := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject: pkix.Name{
+			CommonName:   "sigstore",
+			Organization: []string{"sigstore.dev"},
+		},
+		NotBefore:             time.Now().Add(-10 * time.Minute),
+		NotAfter:              time.Now().Add(5 * time.Hour),
+		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+		BasicConstraintsValid: true,
+		IsCA:                  true,
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err := createCertificate(rootTemplate, rootTemplate, &priv.PublicKey, priv)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cert, priv, nil
+}
+
+func GenerateSubordinateCa(rootTemplate *x509.Certificate, rootPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	subTemplate := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject: pkix.Name{
+			CommonName:   "sigstore-sub",
+			Organization: []string{"sigstore.dev"},
+		},
+		NotBefore:             time.Now().Add(-9 * time.Minute),
+		NotAfter:              time.Now().Add(2 * time.Hour),
+		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+		BasicConstraintsValid: true,
+		IsCA:                  true,
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err := createCertificate(subTemplate, rootTemplate, &priv.PublicKey, rootPriv)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cert, priv, nil
+}
+
+func GenerateLeafCert(subject, oidcIssuer string, uri *url.URL, parentTemplate *x509.Certificate, parentPriv crypto.Signer, exts ...pkix.Extension) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	exts = append(exts, pkix.Extension{
+		// OID for OIDC Issuer extension
+		Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
+		Critical: false,
+		Value:    []byte(oidcIssuer),
+	})
+	certTemplate := &x509.Certificate{
+		SerialNumber:    big.NewInt(1),
+		EmailAddresses:  []string{subject},
+		NotBefore:       time.Now().Add(-1 * time.Minute),
+		NotAfter:        time.Now().Add(time.Hour),
+		KeyUsage:        x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:     []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+		IsCA:            false,
+		ExtraExtensions: exts,
+	}
+	if uri != nil {
+		certTemplate.URIs = []*url.URL{uri}
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cert, priv, nil
+}
+
+func GenerateExpiredLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	certTemplate := &x509.Certificate{
+		SerialNumber:   big.NewInt(1),
+		EmailAddresses: []string{subject},
+		NotBefore:      time.Now().Add(-5 * time.Minute),
+		NotAfter:       time.Now().Add(-2 * time.Minute),
+		KeyUsage:       x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:    []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+		IsCA:           false,
+		ExtraExtensions: []pkix.Extension{{
+			// OID for OIDC Issuer extension
+			Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
+			Critical: false,
+			Value:    []byte(oidcIssuer),
+		}},
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cert, priv, nil
+}
+
+func GenerateLeafCertWithSubjectAlternateNames(dnsNames []string, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, oidcIssuer string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) {
+	certTemplate := &x509.Certificate{
+		SerialNumber:   big.NewInt(1),
+		EmailAddresses: emailAddresses,
+		DNSNames:       dnsNames,
+		IPAddresses:    ipAddresses,
+		URIs:           uris,
+		NotBefore:      time.Now().Add(-1 * time.Minute),
+		NotAfter:       time.Now().Add(time.Hour),
+		KeyUsage:       x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:    []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
+		IsCA:           false,
+		ExtraExtensions: []pkix.Extension{{
+			// OID for OIDC Issuer extension
+			Id:       asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1},
+			Critical: false,
+			Value:    []byte(oidcIssuer),
+		}},
+	}
+
+	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cert, priv, nil
+}
diff --git a/pkg/pki/x509/x509.go b/pkg/pki/x509/x509.go
new file mode 100644
index 00000000..27300498
--- /dev/null
+++ b/pkg/pki/x509/x509.go
@@ -0,0 +1,253 @@
+//
+// Copyright 2025 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.
+
+// Copied with modifications from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/x509.go
+
+package x509
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/hex"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io"
+
+	"github.com/sigstore/rekor/pkg/pki/identity"
+	"github.com/sigstore/sigstore/pkg/cryptoutils"
+	sigsig "github.com/sigstore/sigstore/pkg/signature"
+)
+
+type Signature struct {
+	signature        []byte
+	verifierLoadOpts []sigsig.LoadOption
+}
+
+// NewSignature creates and validates an x509 signature object
+func NewSignature(r io.Reader) (*Signature, error) {
+	return NewSignatureWithOpts(r)
+}
+
+func NewSignatureWithOpts(r io.Reader, opts ...sigsig.LoadOption) (*Signature, error) {
+	b, err := io.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+	return &Signature{
+		signature:        b,
+		verifierLoadOpts: opts,
+	}, nil
+}
+
+// CanonicalValue implements the pki.Signature interface
+func (s Signature) CanonicalValue() ([]byte, error) {
+	return s.signature, nil
+}
+
+// Verify implements the pki.Signature interface
+func (s Signature) Verify(r io.Reader, k interface{}, opts ...sigsig.VerifyOption) error {
+	if len(s.signature) == 0 {
+		//lint:ignore ST1005 X509 is proper use of term
+		return errors.New("X509 signature has not been initialized")
+	}
+
+	key, ok := k.(*PublicKey)
+	if !ok {
+		return fmt.Errorf("invalid public key type for: %v", k)
+	}
+
+	p := key.key
+	if p == nil {
+		switch {
+		case key.cert != nil:
+			p = key.cert.c.PublicKey
+		case len(key.certs) > 0:
+			if err := verifyCertChain(key.certs); err != nil {
+				return err
+			}
+			p = key.certs[0].PublicKey
+		default:
+			return errors.New("no public key found")
+		}
+	}
+
+	verifier, err := sigsig.LoadVerifierWithOpts(p, s.verifierLoadOpts...)
+	if err != nil {
+		return err
+	}
+	return verifier.VerifySignature(bytes.NewReader(s.signature), r, opts...)
+}
+
+// PublicKey Public Key that follows the x509 standard
+type PublicKey struct {
+	key   interface{}
+	cert  *cert
+	certs []*x509.Certificate
+}
+
+type cert struct {
+	c *x509.Certificate
+	b []byte
+}
+
+// NewPublicKey implements the pki.PublicKey interface
+func NewPublicKey(r io.Reader) (*PublicKey, error) {
+	rawPub, err := io.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+	trimmedRawPub := bytes.TrimSpace(rawPub)
+
+	block, rest := pem.Decode(trimmedRawPub)
+	if block == nil {
+		return nil, errors.New("invalid public key: failure decoding PEM")
+	}
+
+	// Handle certificate chain, concatenated PEM-encoded certificates
+	if len(rest) > 0 {
+		// Support up to 10 certificates in a chain, to avoid parsing extremely long chains
+		certs, err := cryptoutils.UnmarshalCertificatesFromPEMLimited(trimmedRawPub, 10)
+		if err != nil {
+			return nil, err
+		}
+		return &PublicKey{certs: certs}, nil
+	}
+
+	switch block.Type {
+	case string(cryptoutils.PublicKeyPEMType):
+		key, err := x509.ParsePKIXPublicKey(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		return &PublicKey{key: key}, nil
+	case string(cryptoutils.CertificatePEMType):
+		c, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		return &PublicKey{
+			cert: &cert{
+				c: c,
+				b: block.Bytes,
+			}}, nil
+	}
+	return nil, fmt.Errorf("invalid public key: cannot handle type %v", block.Type)
+}
+
+// CanonicalValue implements the pki.PublicKey interface
+func (k PublicKey) CanonicalValue() (encoded []byte, err error) {
+
+	switch {
+	case k.key != nil:
+		encoded, err = cryptoutils.MarshalPublicKeyToPEM(k.key)
+	case k.cert != nil:
+		encoded, err = cryptoutils.MarshalCertificateToPEM(k.cert.c)
+	case k.certs != nil:
+		encoded, err = cryptoutils.MarshalCertificatesToPEM(k.certs)
+	default:
+		err = errors.New("x509 public key has not been initialized")
+	}
+
+	return
+}
+
+func (k PublicKey) CryptoPubKey() crypto.PublicKey {
+	if k.cert != nil {
+		return k.cert.c.PublicKey
+	}
+	if len(k.certs) > 0 {
+		return k.certs[0].PublicKey
+	}
+	return k.key
+}
+
+// Subjects implements the pki.PublicKey interface
+func (k PublicKey) Subjects() []string {
+	var subjects []string
+	var cert *x509.Certificate
+	if k.cert != nil {
+		cert = k.cert.c
+	} else if len(k.certs) > 0 {
+		cert = k.certs[0]
+	}
+	if cert != nil {
+		subjects = cryptoutils.GetSubjectAlternateNames(cert)
+	}
+	return subjects
+}
+
+// Identities implements the pki.PublicKey interface
+func (k PublicKey) Identities() ([]identity.Identity, error) {
+	// k contains either a key, a cert, or a list of certs
+	if k.key != nil {
+		pkixKey, err := cryptoutils.MarshalPublicKeyToDER(k.key)
+		if err != nil {
+			return nil, err
+		}
+		digest := sha256.Sum256(pkixKey)
+		return []identity.Identity{{
+			Crypto:      k.key,
+			Raw:         pkixKey,
+			Fingerprint: hex.EncodeToString(digest[:]),
+		}}, nil
+	}
+
+	var cert *x509.Certificate
+	switch {
+	case k.cert != nil:
+		cert = k.cert.c
+	case len(k.certs) > 0:
+		cert = k.certs[0]
+	default:
+		return nil, errors.New("no key, certificate or certificate chain provided")
+	}
+
+	digest := sha256.Sum256(cert.Raw)
+	return []identity.Identity{{
+		Crypto:      cert,
+		Raw:         cert.Raw,
+		Fingerprint: hex.EncodeToString(digest[:]),
+	}}, nil
+}
+
+func verifyCertChain(certChain []*x509.Certificate) error {
+	if len(certChain) == 0 {
+		return errors.New("no certificate chain provided")
+	}
+	// No certificate chain to verify
+	if len(certChain) == 1 {
+		return nil
+	}
+	rootPool := x509.NewCertPool()
+	rootPool.AddCert(certChain[len(certChain)-1])
+	subPool := x509.NewCertPool()
+	for _, c := range certChain[1 : len(certChain)-1] {
+		subPool.AddCert(c)
+	}
+	if _, err := certChain[0].Verify(x509.VerifyOptions{
+		Roots:         rootPool,
+		Intermediates: subPool,
+		// Allow any key usage
+		KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
+		// Expired certificates can be uploaded and should be verifiable
+		CurrentTime: certChain[0].NotBefore,
+	}); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/pkg/pki/x509/x509_test.go b/pkg/pki/x509/x509_test.go
new file mode 100644
index 00000000..4e424120
--- /dev/null
+++ b/pkg/pki/x509/x509_test.go
@@ -0,0 +1,399 @@
+//
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/x509_test.go
+
+//go:debug rsa1024min=0
+
+package x509
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/hex"
+	"net"
+	"net/url"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/sigstore/rekor/pkg/pki/identity"
+	"github.com/sigstore/rekor/pkg/pki/x509/testutils"
+	"github.com/sigstore/sigstore/pkg/cryptoutils"
+	"github.com/sigstore/sigstore/pkg/signature"
+)
+
+// Generated with:
+// openssl genrsa -out myprivate.pem 512
+// openssl pkcs8 -topk8 -in myprivate.pem  -nocrypt'
+const pkcs1v15Priv = `-----BEGIN PRIVATE KEY-----
+MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAoLEL57Kd5w8b5LCl
+SM+5mJbVYj4GoFXP/Gynfk6mDj7aANYWAkU74xkjz0BX2Nq0IT9DyxWI8aXZ8B6R
+YtbsPwIDAQABAkA2WgwTz5eXKsYdgR421YQKN6JvO1mUa9IQqFOy5jlGgbR+W5HG
+JfQVJKhCGMYYmByHgR0QDk/6gvJjhuszTHuJAiEA0siY/vE20zC1UHpPgDXXVSNN
+dKtM6YKBKSo47oTKQHsCIQDDKZgal50Cd3W+lOWpNO23QGZgBhJrJ70TpcPWGEsS
+DQIhAIDIMLnq1G1Z4B2IbRRPUP3icMtscbRlmNZ2xovsM8oLAiBluZh+w+gjEQFe
+hV3wBJajnf2+r2uKTvxO8WhSf/chQQIhAKzYjX2chfvPN6hRqeGeoPpRLXS8cdxC
+A4hZJRvZgkO3
+-----END PRIVATE KEY-----
+`
+
+// Extracted from above with:
+// openssl rsa -in myprivate.pem -pubout
+const pkcs1v15Pub = `-----BEGIN PUBLIC KEY-----
+MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKCxC+eynecPG+SwpUjPuZiW1WI+BqBV
+z/xsp35Opg4+2gDWFgJFO+MZI89AV9jatCE/Q8sViPGl2fAekWLW7D8CAwEAAQ==
+-----END PUBLIC KEY-----
+`
+
+// Generated with:
+// openssl ecparam -genkey -name prime256v1 > ec_private.pem
+// openssl pkcs8 -topk8 -in ec_private.pem  -nocrypt
+const priv = `-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmrLtCpBdXgXLUr7o
+nSUPfo3oXMjmvuwTOjpTulIBKlKhRANCAATH6KSpTFe6uXFmW1qNEFXaO7fWPfZt
+pPZrHZ1cFykidZoURKoYXfkohJ+U/USYy8Sd8b4DMd5xDRZCnlDM0h37
+-----END PRIVATE KEY-----
+`
+
+// Extracted from above with:
+// openssl ec -in ec_private.pem -pubout
+const pubStr = `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx+ikqUxXurlxZltajRBV2ju31j32
+baT2ax2dXBcpInWaFESqGF35KISflP1EmMvEnfG+AzHecQ0WQp5QzNId+w==
+-----END PUBLIC KEY-----
+`
+
+// Generated with:
+// openssl genpkey -algorithm ED25519 -out edprivate.pem
+const ed25519Priv = `-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIKjlXfR/VFvO9qM9+CG2qbuSM54k8ciKWHhgNwKTgqpG
+-----END PRIVATE KEY-----
+`
+
+// Extracted from above with:
+// openssl pkey -in edprivate.pem -pubout
+const ed25519Pub = `-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEAizWek2gKgMM+bad4rVJ5nc9NsbNOba0A0BNfzOgklRs=
+-----END PUBLIC KEY-----
+`
+
+const pubWithTrailingNewLine = `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx+ikqUxXurlxZltajRBV2ju31j32
+baT2ax2dXBcpInWaFESqGF35KISflP1EmMvEnfG+AzHecQ0WQp5QzNId+w==
+-----END PUBLIC KEY-----
+
+`
+
+func signData(t *testing.T, b []byte, pkey string) []byte {
+
+	priv, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(pkey), cryptoutils.SkipPassword)
+	if err != nil {
+		t.Fatal(err)
+	}
+	signer, err := signature.LoadSigner(priv, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	signature, err := signer.SignMessage(bytes.NewReader(b))
+	if err != nil {
+		t.Fatal(err)
+	}
+	return signature
+}
+
+func TestSignature_Verify(t *testing.T) {
+	tests := []struct {
+		name string
+		priv string
+		pub  string
+	}{
+		{
+			name: "rsa",
+			priv: pkcs1v15Priv,
+			pub:  pkcs1v15Pub,
+		},
+		{
+			name: "ec",
+			priv: priv,
+			pub:  pubStr,
+		},
+		{
+			name: "ed25519",
+			priv: ed25519Priv,
+			pub:  ed25519Pub,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			data := []byte("hey! this is my test data")
+			sigBytes := signData(t, data, tt.priv)
+			s, err := NewSignature(bytes.NewReader(sigBytes))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			pub, err := NewPublicKey(strings.NewReader(tt.pub))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if err := s.Verify(bytes.NewReader(data), pub); err != nil {
+				t.Errorf("Signature.Verify() error = %v", err)
+			}
+
+			// Now try with the canonical value
+			cb, err := s.CanonicalValue()
+			if err != nil {
+				t.Error(err)
+			}
+			canonicalSig, err := NewSignature(bytes.NewReader(cb))
+			if err != nil {
+				t.Error(err)
+			}
+			if err := canonicalSig.Verify(bytes.NewReader(data), pub); err != nil {
+				t.Errorf("Signature.Verify() error = %v", err)
+			}
+
+			pubKey, _ := cryptoutils.UnmarshalPEMToPublicKey([]byte(tt.pub))
+			derKey, _ := cryptoutils.MarshalPublicKeyToDER(pubKey)
+			digest := sha256.Sum256(derKey)
+			expectedID := identity.Identity{Crypto: pubKey, Raw: derKey, Fingerprint: hex.EncodeToString(digest[:])}
+			ids, err := pub.Identities()
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(ids) != 1 {
+				t.Errorf("%v: too many identities, expected 1, got %v", tt.name, len(ids))
+			}
+			switch v := ids[0].Crypto.(type) {
+			case *rsa.PublicKey:
+				if tt.name != "rsa" {
+					t.Fatalf("unexpected key, expected RSA, got %v", reflect.TypeOf(v))
+				}
+			case *ecdsa.PublicKey:
+				if tt.name != "ec" {
+					t.Fatalf("unexpected key, expected RSA, got %v", reflect.TypeOf(v))
+				}
+			case ed25519.PublicKey:
+				if tt.name != "ed25519" {
+					t.Fatalf("unexpected key, expected RSA, got %v", reflect.TypeOf(v))
+				}
+			default:
+				t.Fatalf("unexpected key type, got %v", reflect.TypeOf(v))
+			}
+			if err := cryptoutils.EqualKeys(expectedID.Crypto, ids[0].Crypto); err != nil {
+				t.Errorf("%v: public keys did not match: %v", tt.name, err)
+			}
+			if !reflect.DeepEqual(expectedID.Raw, ids[0].Raw) {
+				t.Errorf("%v: raw identities did not match, expected %v, got %v", tt.name, expectedID.Raw, ids[0].Raw)
+			}
+			if expectedID.Fingerprint != ids[0].Fingerprint {
+				t.Errorf("%v: fingerprints did not match, expected %v, got %v", tt.name, expectedID.Fingerprint, ids[0].Fingerprint)
+			}
+		})
+	}
+}
+
+func TestSignature_VerifyFail(t *testing.T) {
+	tests := []struct {
+		name string
+		priv string
+		pub  string
+	}{
+		{
+			name: "rsa",
+			priv: pkcs1v15Priv,
+			pub:  pkcs1v15Pub,
+		},
+		{
+			name: "ec",
+			priv: priv,
+			pub:  pubStr,
+		},
+		{
+			name: "ed25519",
+			priv: ed25519Priv,
+			pub:  ed25519Pub,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Make some fake data, and tamper with the signature
+			data := []byte("hey! this is my test data")
+			sigBytes := signData(t, data, tt.priv)
+			sigBytes[0]--
+			s, err := NewSignature(bytes.NewReader(sigBytes))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			pub, err := NewPublicKey(strings.NewReader(tt.pub))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if err := s.Verify(bytes.NewReader(data), pub); err == nil {
+				t.Error("Signature.Verify() expected error!")
+			}
+		})
+	}
+}
+
+func TestPublicKeyWithCertChain(t *testing.T) {
+	rootCert, rootKey, _ := testutils.GenerateRootCa()
+	subCert, subKey, _ := testutils.GenerateSubordinateCa(rootCert, rootKey)
+	subjectURL, _ := url.Parse("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.1.1")
+	leafCert, leafKey, _ := testutils.GenerateLeafCertWithSubjectAlternateNames(
+		[]string{"example.com"}, []string{"subject@example.com"}, []net.IP{{1, 1, 1, 1}}, []*url.URL{subjectURL}, "oidc-issuer", subCert, subKey)
+
+	pemCertChain, err := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, subCert, rootCert})
+	if err != nil {
+		t.Fatalf("unexpected error marshalling certificate chain: %v", err)
+	}
+
+	pub, err := NewPublicKey(bytes.NewReader(pemCertChain))
+	if err != nil {
+		t.Fatalf("unexpected error generating public key: %v", err)
+	}
+	if pub.certs == nil || !pub.certs[0].Equal(leafCert) || !pub.certs[1].Equal(subCert) || !pub.certs[2].Equal(rootCert) {
+		t.Fatal("expected certificate chain to match provided certificate chain")
+	}
+
+	if !pub.CryptoPubKey().(*ecdsa.PublicKey).Equal(leafKey.Public()) {
+		t.Fatal("expected public keys to match")
+	}
+
+	var expectedSubjects []string
+	expectedSubjects = append(expectedSubjects, leafCert.DNSNames...)
+	expectedSubjects = append(expectedSubjects, leafCert.EmailAddresses...)
+	expectedSubjects = append(expectedSubjects, leafCert.IPAddresses[0].String())
+	expectedSubjects = append(expectedSubjects, leafCert.URIs[0].String())
+	if !reflect.DeepEqual(pub.Subjects(), expectedSubjects) {
+		t.Fatalf("expected matching subjects, expected %v, got %v", expectedSubjects, pub.Subjects())
+	}
+
+	digest := sha256.Sum256(leafCert.Raw)
+	expectedID := identity.Identity{Crypto: leafCert, Raw: leafCert.Raw, Fingerprint: hex.EncodeToString((digest[:]))}
+	ids, err := pub.Identities()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(ids) != 1 {
+		t.Errorf("too many identities, expected 1, got %v", len(ids))
+	}
+	if !ids[0].Crypto.(*x509.Certificate).Equal(expectedID.Crypto.(*x509.Certificate)) {
+		t.Errorf("certificates did not match")
+	}
+	if !reflect.DeepEqual(expectedID.Raw, ids[0].Raw) {
+		t.Errorf("raw identities did not match, expected %v, got %v", expectedID.Raw, ids[0].Raw)
+	}
+	if expectedID.Fingerprint != ids[0].Fingerprint {
+		t.Errorf("fingerprints did not match, expected %v, got %v", expectedID.Fingerprint, ids[0].Fingerprint)
+	}
+
+	canonicalValue, err := pub.CanonicalValue()
+	if err != nil {
+		t.Fatalf("unexpected error fetching canonical value: %v", err)
+	}
+	if !reflect.DeepEqual(canonicalValue, pemCertChain) {
+		t.Fatalf("expected canonical value %v, got %v", pemCertChain, canonicalValue)
+	}
+
+	// Generate signature to verify
+	data := []byte("test")
+	signer, err := signature.LoadSigner(leafKey, crypto.SHA256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sigBytes, err := signer.SignMessage(bytes.NewReader(data))
+	if err != nil {
+		t.Fatal(err)
+	}
+	s, err := NewSignature(bytes.NewReader(sigBytes))
+	if err != nil {
+		t.Fatalf("unexpected error generating signature: %v", err)
+	}
+	err = s.Verify(bytes.NewReader(data), pub)
+	if err != nil {
+		t.Fatalf("unexpected error verifying signature, %v", err)
+	}
+
+	// Verify works with expired certificate
+	leafCert, leafKey, _ = testutils.GenerateExpiredLeafCert("subject@example.com", "oidc-issuer", subCert, subKey)
+	pemCertChain, _ = cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, subCert, rootCert})
+	pub, _ = NewPublicKey(bytes.NewReader(pemCertChain))
+	signer, _ = signature.LoadSigner(leafKey, crypto.SHA256)
+	sigBytes, _ = signer.SignMessage(bytes.NewReader(data))
+	s, _ = NewSignature(bytes.NewReader(sigBytes))
+	err = s.Verify(bytes.NewReader(data), pub)
+	if err != nil {
+		t.Fatalf("unexpected error verifying signature with expired certificate: %v", err)
+	}
+
+	// Verify error with invalid chain
+	pemCertChain, _ = cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, rootCert})
+	pub, _ = NewPublicKey(bytes.NewReader(pemCertChain))
+	signer, _ = signature.LoadSigner(leafKey, crypto.SHA256)
+	sigBytes, _ = signer.SignMessage(bytes.NewReader(data))
+	s, _ = NewSignature(bytes.NewReader(sigBytes))
+	err = s.Verify(bytes.NewReader(data), pub)
+	if err == nil || !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") {
+		t.Fatalf("expected error verifying signature, got %v", err)
+	}
+
+	// Verify works with chain without intermediate
+	leafCert, leafKey, _ = testutils.GenerateLeafCert("subject@example.com", "oidc-issuer", nil, rootCert, rootKey)
+	pemCertChain, _ = cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, rootCert})
+	pub, _ = NewPublicKey(bytes.NewReader(pemCertChain))
+	signer, _ = signature.LoadSigner(leafKey, crypto.SHA256)
+	sigBytes, _ = signer.SignMessage(bytes.NewReader(data))
+	s, _ = NewSignature(bytes.NewReader(sigBytes))
+	err = s.Verify(bytes.NewReader(data), pub)
+	if err != nil {
+		t.Fatalf("unexpected error verifying signature, %v", err)
+	}
+
+	// Verify error with long chain
+	chain := []*x509.Certificate{}
+	for i := 0; i < 11; i++ {
+		chain = append(chain, leafCert)
+	}
+	pemCertChain, _ = cryptoutils.MarshalCertificatesToPEM(chain)
+	_, err = NewPublicKey(bytes.NewReader(pemCertChain))
+	if err == nil || !strings.Contains(err.Error(), "too many certificates specified in PEM block") {
+		t.Fatalf("expected error with long certificate chain, got %v", err)
+	}
+
+	// Verify public key with extra trailing newline is parsed OK
+	key, err := NewPublicKey(strings.NewReader(pubWithTrailingNewLine))
+	if err != nil {
+		t.Fatalf("unexpected error parsing public key with extra trailing newline: %v", err)
+	}
+	canonicalKeyBytes, err := key.CanonicalValue()
+	if err != nil {
+		t.Fatalf("unexpected error canonicalizing public key with extra trailing newline: %v", err)
+	}
+
+	if !bytes.Equal([]byte(pubStr), canonicalKeyBytes) {
+		t.Fatalf("expected canonical value to match original without extra trailing new line")
+	}
+}
diff --git a/pkg/server/service_test.go b/pkg/server/service_test.go
index 9302b150..549a4660 100644
--- a/pkg/server/service_test.go
+++ b/pkg/server/service_test.go
@@ -16,6 +16,8 @@ package server
 
 import (
 	"context"
+	"encoding/base64"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
@@ -45,16 +47,19 @@ func TestCreateEntry(t *testing.T) {
 			req: &pb.CreateEntryRequest{
 				Spec: &pb.CreateEntryRequest_HashedRekordRequest{
 					HashedRekordRequest: &pb.HashedRekordRequest{
-						Signature: []byte("abcd"),
+						Signature: b64DecodeOrDie(t, "MEYCIQC59oLS3MsCqm0xCxPOy+8FdQK4RYCZE036s3q1ECfcagIhAJ4ATXlCSdFrklKAS8No0PsAE9uLi37TCbIfRXASJTTb"),
 						Verifier: &pb.Verifier{
 							Verifier: &pb.Verifier_PublicKey{
 								PublicKey: &pb.PublicKey{
-									RawBytes: []byte("3456"),
+									RawBytes: []byte(`-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeLw7gX40qy1z7JUhGMAaaDITbV7p
+2D+C5G9xPEsy/PVAo9H0mgS4NYzpGirkXxBht+IvvL19WR1X9ANXha5ldQ==
+-----END PUBLIC KEY-----`),
 								},
 							},
 						},
 						Data: &v1.HashOutput{
-							Digest: []byte("ef12"),
+							Digest: hexDecodeOrDie(t, "5b3513f580c8397212ff2c8f459c199efc0c90e4354a5f3533adf0a3fff3a530"),
 						},
 					},
 				},
@@ -66,12 +71,15 @@ func TestCreateEntry(t *testing.T) {
 			req: &pb.CreateEntryRequest{
 				Spec: &pb.CreateEntryRequest_DsseRequest{
 					DsseRequest: &pb.DSSERequest{
-						Envelope: "dsse",
+						Envelope: "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"cGF5bG9hZA==\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIQCSWas1Y9bI7aDNrBdHlzrFH8ch7B7IM+pJK86mtjkbJAIgaeCltz6vs20DP2sJ7IBihvcrdqGn3ivuV/KNPlMOetk=\"}]}",
 						Verifier: []*pb.Verifier{
 							{
 								Verifier: &pb.Verifier_PublicKey{
 									PublicKey: &pb.PublicKey{
-										RawBytes: []byte("3456"),
+										RawBytes: []byte(`-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE850nB+WrwXzivt7yFbhFKw/8M2pa
+qSTHiQhkA4/0ZAsJtmzn/v4HdeZKTCQcsHq5IwM/LtbmEdv9ChO9M3cg9g==
+-----END PUBLIC KEY-----`),
 									},
 								},
 							},
@@ -106,16 +114,19 @@ func TestCreateEntry(t *testing.T) {
 			req: &pb.CreateEntryRequest{
 				Spec: &pb.CreateEntryRequest_HashedRekordRequest{
 					HashedRekordRequest: &pb.HashedRekordRequest{
-						Signature: []byte("abcd"),
+						Signature: b64DecodeOrDie(t, "MEYCIQC59oLS3MsCqm0xCxPOy+8FdQK4RYCZE036s3q1ECfcagIhAJ4ATXlCSdFrklKAS8No0PsAE9uLi37TCbIfRXASJTTb"),
 						Verifier: &pb.Verifier{
 							Verifier: &pb.Verifier_PublicKey{
 								PublicKey: &pb.PublicKey{
-									RawBytes: []byte("3456"),
+									RawBytes: []byte(`-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeLw7gX40qy1z7JUhGMAaaDITbV7p
+2D+C5G9xPEsy/PVAo9H0mgS4NYzpGirkXxBht+IvvL19WR1X9ANXha5ldQ==
+-----END PUBLIC KEY-----`),
 								},
 							},
 						},
 						Data: &v1.HashOutput{
-							Digest: []byte("ef12"),
+							Digest: hexDecodeOrDie(t, "5b3513f580c8397212ff2c8f459c199efc0c90e4354a5f3533adf0a3fff3a530"),
 						},
 					},
 				},
@@ -150,3 +161,19 @@ func (s *mockStorage) Add(_ context.Context, _ *ttessera.Entry) (*rekor_pb.Trans
 func (s *mockStorage) ReadTile(_ context.Context, _, _ uint64, _ uint8) ([]byte, error) {
 	return nil, nil
 }
+
+func hexDecodeOrDie(t *testing.T, hash string) []byte {
+	decoded, err := hex.DecodeString(hash)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return decoded
+}
+
+func b64DecodeOrDie(t *testing.T, msg string) []byte {
+	decoded, err := base64.StdEncoding.DecodeString(msg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return decoded
+}
diff --git a/pkg/types/dsse/dsse.go b/pkg/types/dsse/dsse.go
index 5d6259ad..77cd9298 100644
--- a/pkg/types/dsse/dsse.go
+++ b/pkg/types/dsse/dsse.go
@@ -15,10 +15,19 @@
 package dsse
 
 import (
+	"bytes"
+	"context"
+	"crypto"
+	"encoding/json"
+	"errors"
 	"fmt"
 
+	"github.com/secure-systems-lab/go-securesystemslib/dsse"
 	pb "github.com/sigstore/rekor-tiles/pkg/generated/protobuf"
+	"github.com/sigstore/rekor-tiles/pkg/pki/x509"
 	"github.com/sigstore/rekor-tiles/pkg/types/verifier"
+	"github.com/sigstore/sigstore/pkg/signature"
+	sigdsse "github.com/sigstore/sigstore/pkg/signature/dsse"
 )
 
 func Validate(ds *pb.DSSERequest) error {
@@ -33,5 +42,75 @@ func Validate(ds *pb.DSSERequest) error {
 			return err
 		}
 	}
+	envelope := &dsse.Envelope{}
+	if err := json.Unmarshal([]byte(ds.Envelope), envelope); err != nil {
+		return err
+	}
+	if len(envelope.Signatures) == 0 {
+		return fmt.Errorf("envelope missing signatures")
+	}
+	allPubKeyBytes := make([][]byte, 0)
+	for _, verifier := range ds.Verifier {
+		pubKey := verifier.GetPublicKey()
+		cert := verifier.GetX509Certificate()
+		if pubKey != nil {
+			allPubKeyBytes = append(allPubKeyBytes, pubKey.RawBytes)
+		} else {
+			allPubKeyBytes = append(allPubKeyBytes, cert.RawBytes)
+		}
+	}
+	_, err := verifyEnvelope(allPubKeyBytes, envelope)
+	if err != nil {
+		return err
+	}
 	return 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.
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/types/dsse/v0.0.1/entry.go#L364-L403
+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 {
+		if len(allSigs) == 0 {
+			break // if all signatures have been verified, do not attempt anymore
+		}
+		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(&sigdsse.VerifierAdapter{SignatureVerifier: vfr})
+		if err != nil {
+			return nil, fmt.Errorf("could not use public key as a dsse verifier: %w", err)
+		}
+
+		accepted, err := dsseVfr.Verify(context.Background(), 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/dsse/dsse_test.go b/pkg/types/dsse/dsse_test.go
index 00796fee..24b94ce3 100644
--- a/pkg/types/dsse/dsse_test.go
+++ b/pkg/types/dsse/dsse_test.go
@@ -18,10 +18,32 @@ import (
 	"fmt"
 	"testing"
 
+	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
 	pb "github.com/sigstore/rekor-tiles/pkg/generated/protobuf"
 	"github.com/stretchr/testify/assert"
 )
 
+var (
+	publicKey = `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE850nB+WrwXzivt7yFbhFKw/8M2pa
+qSTHiQhkA4/0ZAsJtmzn/v4HdeZKTCQcsHq5IwM/LtbmEdv9ChO9M3cg9g==
+-----END PUBLIC KEY-----`
+	x509Cert = `-----BEGIN CERTIFICATE-----
+MIICGTCCAb+gAwIBAgIUWi7MFKfQ+/QSDFb0RjUBmyvOCu0wCgYIKoZIzj0EAwIw
+YTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
+dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEaMBgGA1UEAwwRdGVzdC5zaWdzdG9yZS5k
+ZXYwIBcNMjUwMzI3MTgwNjAwWhgPMjEyNTAzMDMxODA2MDBaMGExCzAJBgNVBAYT
+AkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRn
+aXRzIFB0eSBMdGQxGjAYBgNVBAMMEXRlc3Quc2lnc3RvcmUuZGV2MFkwEwYHKoZI
+zj0CAQYIKoZIzj0DAQcDQgAEYJSwH/PInqkK+Um3iMPswCJg9SgypKpWY9onmsAJ
+Sj/nGF5ZiEOLfD7KJ747MtBrQ/lRJTXW5aEs9brVKOwrXqNTMFEwHQYDVR0OBBYE
+FAFlFaiDwXiV0qh7PILjNrp1zdYGMB8GA1UdIwQYMBaAFAFlFaiDwXiV0qh7PILj
+Nrp1zdYGMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhALrgqHZR
+5glHunRCQ60XVtn7xEUvHIkyWdhQvocrEQ+KAiAlucBaXZ5NQ9viz1ATrdSyuj+a
+atI4zS+80vbts4NEFA==
+-----END CERTIFICATE-----`
+)
+
 func TestValidate(t *testing.T) {
 	tests := []struct {
 		name      string
@@ -31,12 +53,12 @@ func TestValidate(t *testing.T) {
 		{
 			name: "valid dsse",
 			dsse: &pb.DSSERequest{
-				Envelope: "dsse",
+				Envelope: "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"cGF5bG9hZA==\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIQCSWas1Y9bI7aDNrBdHlzrFH8ch7B7IM+pJK86mtjkbJAIgaeCltz6vs20DP2sJ7IBihvcrdqGn3ivuV/KNPlMOetk=\"}]}",
 				Verifier: []*pb.Verifier{
 					{
 						Verifier: &pb.Verifier_PublicKey{
 							PublicKey: &pb.PublicKey{
-								RawBytes: []byte("3456"),
+								RawBytes: []byte(publicKey),
 							},
 						},
 					},
@@ -50,7 +72,7 @@ func TestValidate(t *testing.T) {
 					{
 						Verifier: &pb.Verifier_PublicKey{
 							PublicKey: &pb.PublicKey{
-								RawBytes: []byte("3456"),
+								RawBytes: []byte(publicKey),
 							},
 						},
 					},
@@ -61,10 +83,57 @@ func TestValidate(t *testing.T) {
 		{
 			name: "missing verifiers",
 			dsse: &pb.DSSERequest{
-				Envelope: "dsse",
+				Envelope: "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"cGF5bG9hZA==\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIQCSWas1Y9bI7aDNrBdHlzrFH8ch7B7IM+pJK86mtjkbJAIgaeCltz6vs20DP2sJ7IBihvcrdqGn3ivuV/KNPlMOetk=\"}]}",
 			},
 			expectErr: fmt.Errorf("missing verifiers"),
 		},
+		{
+			name: "missing signatures",
+			dsse: &pb.DSSERequest{
+				Envelope: "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"cGF5bG9hZA==\"}",
+				Verifier: []*pb.Verifier{
+					{
+						Verifier: &pb.Verifier_PublicKey{
+							PublicKey: &pb.PublicKey{
+								RawBytes: []byte(publicKey),
+							},
+						},
+					},
+				},
+			},
+			expectErr: fmt.Errorf("envelope missing signatures"),
+		},
+		{
+			name: "invalid signature",
+			dsse: &pb.DSSERequest{
+				Envelope: "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"cGF5bG9hZA==\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"Zm9vYmFyCg==\"}]}",
+				Verifier: []*pb.Verifier{
+					{
+						Verifier: &pb.Verifier_PublicKey{
+							PublicKey: &pb.PublicKey{
+								RawBytes: []byte(publicKey),
+							},
+						},
+					},
+				},
+			},
+			expectErr: fmt.Errorf("could not verify envelope: accepted signatures do not match threshold, Found: 0, Expected 1"),
+		},
+		{
+			name: "valid dsse with X.509 cert",
+			dsse: &pb.DSSERequest{
+				Envelope: "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"cGF5bG9hZA==\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIQDoYuLoinEz/gM6B+hEn/0d47lmRDitQ3LfL9vH0sF/gQIgPqVgoBTRsMSPYMXYuJYYCIaTpnuppqQaTSTRn0ubwLI=\"}]}",
+				Verifier: []*pb.Verifier{
+					{
+						Verifier: &pb.Verifier_X509Certificate{
+							X509Certificate: &v1.X509Certificate{
+								RawBytes: []byte(x509Cert),
+							},
+						},
+					},
+				},
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
diff --git a/pkg/types/hashedrekord/hashedrekord.go b/pkg/types/hashedrekord/hashedrekord.go
index 4edf8917..64298404 100644
--- a/pkg/types/hashedrekord/hashedrekord.go
+++ b/pkg/types/hashedrekord/hashedrekord.go
@@ -15,10 +15,15 @@
 package hashedrekord
 
 import (
+	"bytes"
+	"crypto"
 	"fmt"
 
+	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
 	pb "github.com/sigstore/rekor-tiles/pkg/generated/protobuf"
+	"github.com/sigstore/rekor-tiles/pkg/pki/x509"
 	"github.com/sigstore/rekor-tiles/pkg/types/verifier"
+	"github.com/sigstore/sigstore/pkg/signature/options"
 )
 
 func Validate(hr *pb.HashedRekordRequest) error {
@@ -31,8 +36,39 @@ func Validate(hr *pb.HashedRekordRequest) error {
 	if hr.Data == nil {
 		return fmt.Errorf("missing data")
 	}
-	if len(hr.Data.Digest) == 0 {
+	if hr.Data.Digest == nil {
 		return fmt.Errorf("missing data digest")
 	}
-	return verifier.Validate(hr.Verifier)
+	if err := verifier.Validate(hr.Verifier); err != nil {
+		return err
+	}
+	sigObj, err := x509.NewSignatureWithOpts(bytes.NewReader(hr.Signature), options.WithED25519ph())
+	if err != nil {
+		return fmt.Errorf("parsing signature: %w", err)
+	}
+	var keyObj *x509.PublicKey
+	if pubKey := hr.Verifier.GetPublicKey(); pubKey != nil {
+		keyObj, err = x509.NewPublicKey(bytes.NewReader(pubKey.RawBytes))
+	} else if cert := hr.Verifier.GetX509Certificate(); cert != nil {
+		keyObj, err = x509.NewPublicKey(bytes.NewReader(cert.RawBytes))
+	} else {
+		return fmt.Errorf("must contain either a public key or X.509 certificate")
+	}
+	if err != nil {
+		return fmt.Errorf("parsing public key: %w", err)
+	}
+	var alg crypto.Hash
+	switch hr.Data.Algorithm {
+	case v1.HashAlgorithm_SHA2_384:
+		alg = crypto.SHA384
+	case v1.HashAlgorithm_SHA2_512:
+		alg = crypto.SHA512
+	default:
+		alg = crypto.SHA256
+	}
+	if err := sigObj.Verify(nil, keyObj, options.WithDigest(hr.Data.Digest), options.WithCryptoSignerOpts(alg)); err != nil {
+		return fmt.Errorf("verifying signature: %w", err)
+	}
+
+	return nil
 }
diff --git a/pkg/types/hashedrekord/hashedrekord_test.go b/pkg/types/hashedrekord/hashedrekord_test.go
index 35d5ee29..59935b6a 100644
--- a/pkg/types/hashedrekord/hashedrekord_test.go
+++ b/pkg/types/hashedrekord/hashedrekord_test.go
@@ -15,6 +15,8 @@
 package hashedrekord
 
 import (
+	"encoding/base64"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
@@ -23,6 +25,29 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+var (
+	b64EncodedSignature = "MEYCIQC59oLS3MsCqm0xCxPOy+8FdQK4RYCZE036s3q1ECfcagIhAJ4ATXlCSdFrklKAS8No0PsAE9uLi37TCbIfRXASJTTb"
+	hexEncodedDigest    = "5b3513f580c8397212ff2c8f459c199efc0c90e4354a5f3533adf0a3fff3a530"
+	publicKey           = `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeLw7gX40qy1z7JUhGMAaaDITbV7p
+2D+C5G9xPEsy/PVAo9H0mgS4NYzpGirkXxBht+IvvL19WR1X9ANXha5ldQ==
+-----END PUBLIC KEY-----`
+	x509Cert = `-----BEGIN CERTIFICATE-----
+MIICGTCCAb+gAwIBAgIUbzTFcv75teYBpaXsDOoYUv5GgWgwCgYIKoZIzj0EAwIw
+YTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
+dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEaMBgGA1UEAwwRdGVzdC5zaWdzdG9yZS5k
+ZXYwIBcNMjUwMzI3MTc1MDQ2WhgPMjEyNTAzMDMxNzUwNDZaMGExCzAJBgNVBAYT
+AkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRn
+aXRzIFB0eSBMdGQxGjAYBgNVBAMMEXRlc3Quc2lnc3RvcmUuZGV2MFkwEwYHKoZI
+zj0CAQYIKoZIzj0DAQcDQgAEeLw7gX40qy1z7JUhGMAaaDITbV7p2D+C5G9xPEsy
+/PVAo9H0mgS4NYzpGirkXxBht+IvvL19WR1X9ANXha5ldaNTMFEwHQYDVR0OBBYE
+FJvvzmcqq8f+AiXCgAlE4IgdwTq+MB8GA1UdIwQYMBaAFJvvzmcqq8f+AiXCgAlE
+4IgdwTq+MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgAqzN4Noq
+eH2jEARoIeXY0SRKnaNhVvullmremGvvd6QCIQDrL1WI3a7m8rlHN/7vvCCGtep1
+fRnK+CuN46tvzGu+9A==
+-----END CERTIFICATE-----`
+)
+
 func TestValidate(t *testing.T) {
 	tests := []struct {
 		name         string
@@ -32,16 +57,16 @@ func TestValidate(t *testing.T) {
 		{
 			name: "valid hashedrekord",
 			hashedrekord: &pb.HashedRekordRequest{
-				Signature: []byte("abcd"),
+				Signature: b64DecodeOrDie(t, b64EncodedSignature),
 				Verifier: &pb.Verifier{
 					Verifier: &pb.Verifier_PublicKey{
 						PublicKey: &pb.PublicKey{
-							RawBytes: []byte("3456"),
+							RawBytes: []byte(publicKey),
 						},
 					},
 				},
 				Data: &v1.HashOutput{
-					Digest: []byte("ef12"),
+					Digest: hexDecodeOrDie(t, hexEncodedDigest),
 				},
 			},
 		},
@@ -51,12 +76,12 @@ func TestValidate(t *testing.T) {
 				Verifier: &pb.Verifier{
 					Verifier: &pb.Verifier_PublicKey{
 						PublicKey: &pb.PublicKey{
-							RawBytes: []byte("3456"),
+							RawBytes: []byte(publicKey),
 						},
 					},
 				},
 				Data: &v1.HashOutput{
-					Digest: []byte("ef12"),
+					Digest: hexDecodeOrDie(t, hexEncodedDigest),
 				},
 			},
 			expectErr: fmt.Errorf("missing signature"),
@@ -64,9 +89,9 @@ func TestValidate(t *testing.T) {
 		{
 			name: "missing verifier",
 			hashedrekord: &pb.HashedRekordRequest{
-				Signature: []byte("abcd"),
+				Signature: b64DecodeOrDie(t, b64EncodedSignature),
 				Data: &v1.HashOutput{
-					Digest: []byte("ef12"),
+					Digest: hexDecodeOrDie(t, hexEncodedDigest),
 				},
 			},
 			expectErr: fmt.Errorf("missing verifier"),
@@ -74,11 +99,11 @@ func TestValidate(t *testing.T) {
 		{
 			name: "missing data",
 			hashedrekord: &pb.HashedRekordRequest{
-				Signature: []byte("abcd"),
+				Signature: b64DecodeOrDie(t, b64EncodedSignature),
 				Verifier: &pb.Verifier{
 					Verifier: &pb.Verifier_PublicKey{
 						PublicKey: &pb.PublicKey{
-							RawBytes: []byte("3456"),
+							RawBytes: []byte(publicKey),
 						},
 					},
 				},
@@ -88,11 +113,11 @@ func TestValidate(t *testing.T) {
 		{
 			name: "missing data digest",
 			hashedrekord: &pb.HashedRekordRequest{
-				Signature: []byte("abcd"),
+				Signature: b64DecodeOrDie(t, b64EncodedSignature),
 				Verifier: &pb.Verifier{
 					Verifier: &pb.Verifier_PublicKey{
 						PublicKey: &pb.PublicKey{
-							RawBytes: []byte("3456"),
+							RawBytes: []byte(publicKey),
 						},
 					},
 				},
@@ -100,6 +125,39 @@ func TestValidate(t *testing.T) {
 			},
 			expectErr: fmt.Errorf("missing data digest"),
 		},
+		{
+			name: "invalid signature",
+			hashedrekord: &pb.HashedRekordRequest{
+				Signature: []byte("foobar"),
+				Verifier: &pb.Verifier{
+					Verifier: &pb.Verifier_PublicKey{
+						PublicKey: &pb.PublicKey{
+							RawBytes: []byte(publicKey),
+						},
+					},
+				},
+				Data: &v1.HashOutput{
+					Digest: hexDecodeOrDie(t, hexEncodedDigest),
+				},
+			},
+			expectErr: fmt.Errorf("verifying signature: "),
+		},
+		{
+			name: "valid hashedrekord with X.509 cert",
+			hashedrekord: &pb.HashedRekordRequest{
+				Signature: b64DecodeOrDie(t, b64EncodedSignature),
+				Verifier: &pb.Verifier{
+					Verifier: &pb.Verifier_X509Certificate{
+						X509Certificate: &v1.X509Certificate{
+							RawBytes: []byte(x509Cert),
+						},
+					},
+				},
+				Data: &v1.HashOutput{
+					Digest: hexDecodeOrDie(t, hexEncodedDigest),
+				},
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
@@ -112,3 +170,19 @@ func TestValidate(t *testing.T) {
 		})
 	}
 }
+
+func hexDecodeOrDie(t *testing.T, hash string) []byte {
+	decoded, err := hex.DecodeString(hash)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return decoded
+}
+
+func b64DecodeOrDie(t *testing.T, msg string) []byte {
+	decoded, err := base64.StdEncoding.DecodeString(msg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return decoded
+}


Reviews:
user: login: jku
body: seems ok to me (one question in code though):
* please decide merge order with @loosebazooka 
* let's have a good look at both test coverage (with an eye for more than code line coverage) and potentially simplifying these critical verification methods after these big PRs are in
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T12:12:46Z
user: login: loosebazooka
body: Seems fine to me if we're mostly just copying over from rekor
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T15:48:54Z
user: login: cmurphy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:39:10Z
user: login: cmurphy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:40:08Z
user: login: cmurphy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:42:13Z
user: login: jku
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T08:06:12Z

Review comments:
diff_hunk: @@ -31,8 +36,39 @@ func Validate(hr *pb.HashedRekordRequest) error {
 	if hr.Data == nil {
 		return fmt.Errorf("missing data")
 	}
-	if len(hr.Data.Digest) == 0 {
+	if hr.Data.Digest == nil {
 		return fmt.Errorf("missing data digest")
 	}
-	return verifier.Validate(hr.Verifier)
+	if err := verifier.Validate(hr.Verifier); err != nil {
+		return err
+	}
+	sigObj, err := x509.NewSignatureWithOpts(bytes.NewReader(hr.Signature), options.WithED25519ph())
+	if err != nil {
+		return fmt.Errorf("parsing signature: %w", err)
+	}
+	var keyObj *x509.PublicKey
+	if pubKey := hr.Verifier.GetPublicKey(); pubKey != nil {
+		keyObj, err = x509.NewPublicKey(bytes.NewReader(pubKey.RawBytes))
+	} else if cert := hr.Verifier.GetX509Certificate(); cert != nil {
+		keyObj, err = x509.NewPublicKey(bytes.NewReader(cert.RawBytes))
path: pkg/types/hashedrekord/hashedrekord.go
user: login: jku
body: does this work? `x509.NewPublicKey` is called with both a certificate and a public key as argument? 

(same question applies to dsse)
created_at: 2025-03-27T11:46:24Z
author_association: MEMBER
diff_hunk: @@ -33,5 +42,75 @@ func Validate(ds *pb.DSSERequest) error {
 			return err
 		}
 	}
+	envelope := &dsse.Envelope{}
+	if err := json.Unmarshal([]byte(ds.Envelope), envelope); err != nil {
+		return err
+	}
+	if len(envelope.Signatures) == 0 {
+		return fmt.Errorf("envelope missing signatures")
+	}
+	allPubKeyBytes := make([][]byte, 0)
+	for _, verifier := range ds.Verifier {
+		pubKey := verifier.GetPublicKey()
+		cert := verifier.GetX509Certificate()
+		if pubKey != nil {
+			allPubKeyBytes = append(allPubKeyBytes, pubKey.RawBytes)
+		} else {
+			allPubKeyBytes = append(allPubKeyBytes, cert.RawBytes)
+		}
+	}
+	_, err := verifyEnvelope(allPubKeyBytes, envelope)
+	if err != nil {
+		return err
+	}
 	return 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.
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/types/dsse/v0.0.1/entry.go#L364-L403
+func verifyEnvelope(allPubKeyBytes [][]byte, env *dsse.Envelope) (map[string]*x509.PublicKey, error) {
path: pkg/types/dsse/dsse.go
user: login: jku
body: I really hope this function can be simplified (the underlying EnvelopeVerifier can already handle a set of keys so it should be possible) That said copying existing rekor code seems fine for now.
created_at: 2025-03-27T12:06:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,278 @@
+//
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/x509.go
+
+package x509
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/asn1"
+	"encoding/hex"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/asaskevich/govalidator"
path: pkg/pki/x509/x509.go
user: login: loosebazooka
body: It looks like this hasn't had a release since 2021, maybe that's okay? It doesn't look abandoned.
created_at: 2025-03-27T15:44:36Z
author_association: MEMBER
diff_hunk: @@ -33,5 +42,75 @@ func Validate(ds *pb.DSSERequest) error {
 			return err
 		}
 	}
+	envelope := &dsse.Envelope{}
+	if err := json.Unmarshal([]byte(ds.Envelope), envelope); err != nil {
+		return err
+	}
+	if len(envelope.Signatures) == 0 {
+		return fmt.Errorf("envelope missing signatures")
+	}
+	allPubKeyBytes := make([][]byte, 0)
+	for _, verifier := range ds.Verifier {
+		pubKey := verifier.GetPublicKey()
+		cert := verifier.GetX509Certificate()
+		if pubKey != nil {
+			allPubKeyBytes = append(allPubKeyBytes, pubKey.RawBytes)
+		} else {
+			allPubKeyBytes = append(allPubKeyBytes, cert.RawBytes)
+		}
+	}
+	_, err := verifyEnvelope(allPubKeyBytes, envelope)
+	if err != nil {
+		return err
+	}
 	return 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.
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/types/dsse/v0.0.1/entry.go#L364-L403
+func verifyEnvelope(allPubKeyBytes [][]byte, env *dsse.Envelope) (map[string]*x509.PublicKey, error) {
path: pkg/types/dsse/dsse.go
user: login: loosebazooka
body: I have an update for this in #144. I think the provider of the dsserequest should provide matching sigs/verifier pairs instead of us having to calculate this on the server side.
created_at: 2025-03-27T15:46:02Z
author_association: MEMBER
diff_hunk: @@ -31,8 +36,39 @@ func Validate(hr *pb.HashedRekordRequest) error {
 	if hr.Data == nil {
 		return fmt.Errorf("missing data")
 	}
-	if len(hr.Data.Digest) == 0 {
+	if hr.Data.Digest == nil {
 		return fmt.Errorf("missing data digest")
 	}
-	return verifier.Validate(hr.Verifier)
+	if err := verifier.Validate(hr.Verifier); err != nil {
+		return err
+	}
+	sigObj, err := x509.NewSignatureWithOpts(bytes.NewReader(hr.Signature), options.WithED25519ph())
+	if err != nil {
+		return fmt.Errorf("parsing signature: %w", err)
+	}
+	var keyObj *x509.PublicKey
+	if pubKey := hr.Verifier.GetPublicKey(); pubKey != nil {
+		keyObj, err = x509.NewPublicKey(bytes.NewReader(pubKey.RawBytes))
+	} else if cert := hr.Verifier.GetX509Certificate(); cert != nil {
+		keyObj, err = x509.NewPublicKey(bytes.NewReader(cert.RawBytes))
path: pkg/types/hashedrekord/hashedrekord.go
user: login: cmurphy
body: It should yes https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/x509.go#L135-L151
created_at: 2025-03-27T17:39:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,5 +42,75 @@ func Validate(ds *pb.DSSERequest) error {
 			return err
 		}
 	}
+	envelope := &dsse.Envelope{}
+	if err := json.Unmarshal([]byte(ds.Envelope), envelope); err != nil {
+		return err
+	}
+	if len(envelope.Signatures) == 0 {
+		return fmt.Errorf("envelope missing signatures")
+	}
+	allPubKeyBytes := make([][]byte, 0)
+	for _, verifier := range ds.Verifier {
+		pubKey := verifier.GetPublicKey()
+		cert := verifier.GetX509Certificate()
+		if pubKey != nil {
+			allPubKeyBytes = append(allPubKeyBytes, pubKey.RawBytes)
+		} else {
+			allPubKeyBytes = append(allPubKeyBytes, cert.RawBytes)
+		}
+	}
+	_, err := verifyEnvelope(allPubKeyBytes, envelope)
+	if err != nil {
+		return err
+	}
 	return 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.
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/types/dsse/v0.0.1/entry.go#L364-L403
+func verifyEnvelope(allPubKeyBytes [][]byte, env *dsse.Envelope) (map[string]*x509.PublicKey, error) {
path: pkg/types/dsse/dsse.go
user: login: cmurphy
body: Will keep this for now and @loosebazooka can remove it when the type is updated
created_at: 2025-03-27T17:40:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,278 @@
+//
+// Copyright 2025 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.
+
+// Copied from https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/x509/x509.go
+
+package x509
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/asn1"
+	"encoding/hex"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/asaskevich/govalidator"
path: pkg/pki/x509/x509.go
user: login: cmurphy
body: Actually I can remove this, it looks like it's only used to verify EmailAddresses and there's a note saying that was deprecated https://github.com/sigstore/rekor/blob/73dba7c07d0747f00119417fc0ff994a393f97b2/pkg/pki/pki.go#L28
created_at: 2025-03-27T17:42:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-26T22:37:58Z
body: ## [Codecov](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) Report
Attention: Patch coverage is `38.80208%` with `235 lines` in your changes missing coverage. Please review.
> Project coverage is 31.32%. Comparing base [(`13271f9`)](https://app.codecov.io/gh/sigstore/rekor-tiles/commit/13271f97238bca56f8e2e85747e633a7fdd1e770?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) to head [(`5bbc4cc`)](https://app.codecov.io/gh/sigstore/rekor-tiles/commit/5bbc4cc11aba1c452cb87538e09584456eb93d6d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).
> Report is 6 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) | Patch % | Lines |
|---|---|---|
| [pkg/pki/x509/testutils/cert\_test\_utils.go](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&filepath=pkg%2Fpki%2Fx509%2Ftestutils%2Fcert_test_utils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3BraS94NTA5L3Rlc3R1dGlscy9jZXJ0X3Rlc3RfdXRpbHMuZ28=) | 0.00% | [141 Missing :warning: ](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) |
| [pkg/pki/x509/x509.go](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&filepath=pkg%2Fpki%2Fx509%2Fx509.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3BraS94NTA5L3g1MDkuZ28=) | 61.39% | [48 Missing and 13 partials :warning: ](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) |
| [pkg/types/dsse/dsse.go](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&filepath=pkg%2Ftypes%2Fdsse%2Fdsse.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL2Rzc2UvZHNzZS5nbw==) | 68.51% | [11 Missing and 6 partials :warning: ](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) |
| [pkg/types/hashedrekord/hashedrekord.go](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&filepath=pkg%2Ftypes%2Fhashedrekord%2Fhashedrekord.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3R5cGVzL2hhc2hlZHJla29yZC9oYXNoZWRyZWtvcmQuZ28=) | 48.38% | [12 Missing and 4 partials :warning: ](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #146      +/-   ##
==========================================
+ Coverage   26.84%   31.32%   +4.47%     
==========================================
  Files          29       35       +6     
  Lines        1892     2442     +550     
==========================================
+ Hits          508      765     +257     
- Misses       1346     1606     +260     
- Partials       38       71      +33     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sigstore/rekor-tiles/pull/146?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: cmurphy
created_at: 2025-03-26T22:38:03Z
body: Can rebase on https://github.com/sigstore/rekor-tiles/pull/144 if that goes in first
author_association: CONTRIBUTOR
user: login: loosebazooka
created_at: 2025-03-27T12:35:50Z
body: I can merge last. I'd like to talk through the new protos in our sync today
author_association: MEMBER
user: login: cmurphy
created_at: 2025-03-27T18:13:10Z
body: Removed the deprecated x509 interface method and added tests for x.509 verifiers.
author_association: CONTRIBUTOR
user: login: Hayden-IO
created_at: 2025-03-28T03:27:30Z
body: Quick comment, this looks good. There's a few things I'd like to change in the copied files, such as cleaning up hardcoded sha256 refs and dropping support for cert chains, but I'd rather this get merged with minimal changes to the copied files and we follow up later. Feel free to create an issue and assign it to me for following up.
author_association: CONTRIBUTOR
user: login: cmurphy
created_at: 2025-03-28T17:46:18Z
body: @haydentherapper https://github.com/sigstore/rekor-tiles/issues/156
author_association: CONTRIBUTOR