Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ED25519ph for sign/verify(-blob) commands #3479

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

ret2libc
Copy link

@ret2libc ret2libc commented Jan 15, 2024

Summary

Integrate sigstore/sigstore#1595 into cosign, allowing sign-blob/verify-blob commmands to use ED25519ph as necessary.

This needs both sigstore/rekor#1959 and sigstore/rekor#1945

How I tested this:

# regular ecdsa + sha256
~/projects/sigstore/cosign/cosign sign-blob --rekor-url http://127.0.0.1:3000 --bundle cosign.bundle --yes msg.txt
~/projects/sigstore/cosign/cosign verify-blob --rekor-url http://127.0.0.1:3000 --bundle cosign.bundle --certificate-identity riccardo.schirone@trailofbits.com --certificate-oidc-issuer-regexp '.*google.*' msg.txt

# ed25519ph
openssl genpkey -algorithm ed25519 -out private.pem
~/projects/sigstore/cosign/cosign import-key-pair --key private.pem
~/projects/sigstore/cosign/cosign sign-blob --rekor-url http://127.0.0.1:3000 --bundle cosign.bundle --key import-cosign.key --yes msg.txt
~/projects/sigstore/cosign/cosign verify-blob --rekor-url http://127.0.0.1:3000 --bundle cosign.bundle --certificate-identity riccardo.schirone@trailofbits.com --certificate-oidc-issuer-regexp '.*google.*' --key import-cosign.pub msg.txt

# ed25519ph (with custom fulcio from https://github.com/sigstore/fulcio/pull/1517)
openssl genpkey -algorithm ed25519 -out private.pem
~/projects/sigstore/cosign/cosign import-key-pair --key private.pem
~/projects/sigstore/cosign/cosign sign-blob --fulcio-url http://127.0.0.1:5555 --rekor-url http://127.0.0.1:3000 --bundle cosign.bundle --key import-cosign.key --issue-certificate --yes msg.txt
~/projects/sigstore/cosign/cosign verify-blob --rekor-url http://127.0.0.1:3000 --bundle cosign.bundle --certificate-identity riccardo.schirone@trailofbits.com --certificate-oidc-issuer-regexp '.*google.*' --key import-cosign.pub msg.txt

Release Note

  • Added LoadPrivateKeyWithOpts, TLogUploadWithCustomHash, ValidateAndUnpackCertWithOpts, VerifierForKeyRefWithOpts, LoadPublicKeyRawWithOpts, SignerVerifierFromKeyRefWithOpts, PublicKeyFromKeyRefWithOpts for passing sigstore.signature.SignerVerifierOptions around
  • Allow to support ed25519 keys + sha512

Documentation

@haydentherapper
Copy link
Contributor

Will review once 1595 is agreed upon and merged

Copy link
Author

@ret2libc ret2libc left a comment

Choose a reason for hiding this comment

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

I've added quite a bit of ..WithOpts methods to avoid a major breaking change. Let me know what you think about it and if it clutters the API too much.

Comment on lines +43 to +59
func getHashAlgorithmFromSignerVerifier(sv *SignerVerifier) (crypto.Hash, error) {
publicKey, err := sv.SignerVerifier.PublicKey()
if err != nil {
return crypto.Hash(0), err
}

switch publicKey.(type) {
case *ecdsa.PublicKey:
return crypto.SHA256, nil
case *rsa.PublicKey:
return crypto.SHA256, nil
case ed25519.PublicKey:
return crypto.SHA512, nil
default:
return crypto.Hash(0), fmt.Errorf("unsupported public key type")
}
}
Copy link
Author

Choose a reason for hiding this comment

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

I believe this would be solved with sigstore/sigstore#860 . Shall I add a reference to this issue in the code (and back-ref)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you be interested in taking a look at the failing test in sigstore/sigstore#1426? I think it's just a failure due to a mock not having an expectation met. We could get that merged in then to use here.

Copy link
Author

Choose a reason for hiding this comment

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

Isn't sigstore/sigstore#1426 just about the KMS SignerVerifier? I think we'd need that HashFunc feature at the sigstore.SignerVerifier level.

@ret2libc ret2libc changed the title Add LoadSignerVerifier types/opts for more flexibility Add support for ED25519ph for sign/verify(-blob) commands Jan 18, 2024
@ret2libc
Copy link
Author

@haydentherapper good call on the rekord vs hashedrekord issue!

Currently there are problems indeed and with this change we can't verify rekord types with ed25519 keys

# generate the ed25519 key
openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -pubout -in private.pem -out public.pem

# manually sign the artifact with openssl
openssl pkeyutl -sign -inkey private.pem -out signature.bin -rawin -in msg.txt

# upload the rekord type
rekor-cli upload --artifact msg.txt --pki-format x509 --signature signature.bin --public-key ./public.pem

# <extra-steps to convert the JSON file returned by rekor into a cosign bundle similar to this>
{
  "base64Signature": "PHlSHaBNmd/vAQiC2L4rubGO9t3yZPiaed0+DeiUWFFP6kuntS+glRl1TLeRe6PwKgnmB9NzliaP1AaOmviOCA==",
  "rekorBundle": {
    "SignedEntryTimestamp": "MEUCIQDix9aEGOh5FH9MWi5UF4tUeQiuEQk4N3LIQO3zclktxAIgMsYhdWtysuXInMsHSGtr6aJnqEI9fuXo2YVPesHu+8M=",
    "Payload": {
      "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5NGJmMzQ5ZTg0MGZlNDY2ZjE2YjEzMzIwYjYyYmQ5OGJiMWMzNzQ1YzNkM2I0NjJmNjU2N2U2YjE5MmYwNTEwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6IlBIbFNIYUJObWQvdkFRaUMyTDRydWJHTzl0M3laUGlhZWQwK0RlaVVXRkZQNmt1bnRTK2dsUmwxVExlUmU2UHdLZ25tQjlOemxpYVAxQWFPbXZpT0NBPT0iLCJmb3JtYXQiOiJ4NTA5IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJRVlVKTVNVTWdTMFZaTFMwdExTMEtUVU52ZDBKUldVUkxNbFozUVhsRlFXaEZVR1JvYnpWcFYyY3lUSGRMZFU1clVqZHhZV1Y0ZUVGNk5sWm5ObXBvVG5GR2JuQmlaakZhZEUwOUNpMHRMUzB0UlU1RUlGQlZRa3hKUXlCTFJWa3RMUzB0TFFvPSJ9fX19",
      "integratedTime": 1705591712,
      "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
      "logIndex": 64611518,
      "verification": {
        "inclusionProof": {
          "checkpoint": "rekor.sigstore.dev - 2605736670972794746\n60450541\nTDdmzIGw7irnOEVmst1xfLwbdbTidDA2TcCCxsLHcXU=\nTimestamp: 1705592626690063695\n\n— rekor.sigstore.dev wNI9ajBFAiEAsVX1bwe3gy2zM0qoetaiUxhcim+skErWtqJ5jYwRFPQCIHbOyX1ROm62cPA5zcR4Uk0qBo8xnFb32Eeq6pENgsKJ\n",
          "hashes": [
            "48a7fae1fe3d87d00163531f0bdf4db0a415dc5811033240dfb59ea1ff468720",
            "af02b718919f29619aa10e2095380a0e1098d0764db7ecdf373fa49045fe3a93",
            "d949205967d6d71a948a21882809386d5a96afbd6a8e3f2ae0c856277a7fd486",
            "9e3949e0f4facadf599ec90f7625082d4c5132df557f59c3d32e0be395abbb60",
            "fcb2067298873f0261dc17371b0e86ba81fcf47006d30bdbdd37990c7372b5d3",
            "3083fbf818a23f53ff764c0bde1465a7ea980d9d0a1b00c16272bcbc3383e4f7",
            "cf4ea6c9e0b7709a81bc123b8dcda55b1cbd740780bec340f7958a3435fd5bb7",
            "5f0b22b8608227fe3fc812e24bbf76a4c0476aca1ad93c82aeb995fb942a1a2d",
            "624f898e47d8899786b65032aa22deaa067d35a7a7327283cdd9cdbc9a1dcca6",
            "28d1b7b76f6241fcaef1ac0e4c57f2c5d86a398f3da2595ed6566cbe4cca739b",
            "1251a2143406c85dbed29faa82f1836ae7018cf7116c302324c382012367a619",
            "6feda92de81b963a96fa33036f9f7b63669bdcd5db9768aaccc2219ee63ec49f",
            "79696d716b43c3cb90d5e7981d668767a56d49095c2db6cc50cbf6f06923ef71",
            "a720ca2d28787c3d9d717402148cc1905f521cf672c1b06db393bce70dd2659c",
            "c2165e99b4e4395ccf5e0688ab69f6fb0e5924cf645aabca3d3909c6ffed6053",
            "dc88b3048c9b61970e5a0ef7b9aff0efe41327f2b51fa63abb7a1cf7d7f1c762",
            "f1e50dbab480fa2f6bb0829c6e6acad31d2a37d1fa2daf47f33ecd9ef03aed3f",
            "9f1efda8fe9a51f5e067a3b8e270b53c039f796f14ac326b1385448bbc684556",
            "51e5d80682cc50abdb392ed3a0cb1aa1b946e1f4bff103d04d314620155e13bd",
            "98c486feb5d87092a78a46c4b5be04868654900affc2e86ffb20074dc73a883a",
            "6969c49bd73f19bf28a5eaeabd331ddd60502defb2cd3d96e17b741c80adec6c"
          ],
          "logIndex": 60448087,
          "rootHash": "4c3766cc81b0ee2ae7384566b2dd717cbc1b75b4e27430364dc082c6c2c77175",
          "treeSize": 60450541
        }
      }
    }
  }
}

# using released cosign
cosign verify-blob --bundle=rekor.out.bundle --key public.pem  msg.txt
Verified OK

# using this branch
cosign verify-blob --bundle=rekor.out.bundle --key public.pem  msg.txt
Error: failed to verify signature: ed25519: invalid signature
main.go:74: error during command execution: failed to verify signature: ed25519: invalid signature

The reason is that ed25519ph is assumed in all sign-blob/verify-blob paths. I'm going to try and fix it.

@woodruffw
Copy link
Member

The reason is that ed25519ph is assumed in all sign-blob/verify-blob paths. I'm going to try and fix it.

I'm not 100% confident here, but at least in the bundle case you should be able to decode .rekorBundle.Payload.body and specialize the signing algorithm type if kind == "rekord", I think.

@ret2libc
Copy link
Author

What do you think about this solution?

diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go
index b7d15e2c..17627188 100644
--- a/pkg/cosign/verify.go
+++ b/pkg/cosign/verify.go
@@ -19,6 +19,7 @@ import (
        "context"
        "crypto"
        "crypto/ecdsa"
+       "crypto/ed25519"
        "crypto/sha256"
        "crypto/sha512"
        "crypto/x509"
@@ -233,7 +234,7 @@ func verifyOCISignature(ctx context.Context, verifier signature.Verifier, sig pa
        if err != nil {
                return err
        }
-       signature, err := base64.StdEncoding.DecodeString(b64sig)
+       decodedSignature, err := base64.StdEncoding.DecodeString(b64sig)
        if err != nil {
                return err
        }
@@ -241,7 +242,33 @@ func verifyOCISignature(ctx context.Context, verifier signature.Verifier, sig pa
        if err != nil {
                return err
        }
-       return verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx))
+       // For compatibility reasons, if ED25519ph is used, we try both ED25519 and ED25519ph.
+       // Refusing to verify ED25519 signatures (used e.g. by rekord entries) would break compatibility.
+       // The signature algorithm to use should be uniquely determined before this point.
+       verificationErr := verifier.VerifySignature(bytes.NewReader(decodedSignature), bytes.NewReader(payload), options.WithContext(ctx))
+       if verificationErr == nil {
+               return nil
+       }
+
+       switch verifier.(type) {
+       case *signature.ED25519phVerifier:
+               publicKey, err := verifier.PublicKey()
+               if err != nil {
+                       return err
+               }
+
+               if edPublicKey, ok := publicKey.(ed25519.PublicKey); ok {
+                       altVerifier, err := signature.LoadED25519Verifier(edPublicKey)
+                       if err != nil {
+                               return err
+                       }
+
+                       fmt.Fprintf(os.Stderr, "Failed to verify signature with ED25519ph, falling back to ED25519 for backward compatibility.\n")
+                       verificationErr = altVerifier.VerifySignature(bytes.NewReader(decodedSignature), bytes.NewReader(payload), options.WithContext(ctx))
+               }
+       }
+
+       return verificationErr
 }

@woodruffw
Copy link
Member

What do you think about this solution?

Trial verification is nasty, but I think it's sound in this case (the classic examples of it being unsound are symmetric/asymmetric confusion, like in JWTs). So I'm not a huge fan of it, but it's probably an acceptable tradeoff vs. a backwards-incompatible change.

CCing @haydentherapper for thoughts as well.

@haydentherapper
Copy link
Contributor

I'm good with this approach to attempt verification twice. The other option would be plumbing through which rekor type is being verified, and only use the -ph variant for hashedrekord. I suspect that's gonna be messy though.

Copy link
Member

@woodruffw woodruffw left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks @ret2libc!

@haydentherapper
Copy link
Contributor

Will take a closer look Monday, but overall LGTM

@haydentherapper
Copy link
Contributor

haydentherapper commented Jan 29, 2024

This is also blocked by Fulcio support, correct? Either we'll need to be OK with Fulcio certifying ed25519ph keys as ed25519 (my preference), or fork x509.go

edit: sorry, ignore part of this: fulcio is not a blocker assuming we are good with fulcio certifying ed25519ph keys as ed25519

@ret2libc
Copy link
Author

This is also blocked by Fulcio support, correct? Either we'll need to be OK with Fulcio certifying ed25519ph keys as ed25519 (my preference), or fork x509.go

edit: sorry, ignore part of this: fulcio is not a blocker assuming we are good with fulcio certifying ed25519ph keys as ed25519

The only real problem for Fulcio is the hash function at this point. See https://github.com/sigstore/fulcio/pull/1517/files#diff-648d47fb9eeb444c1a09095dd41e4012ee5aafcb37b712f7f3bf492d8410017dR149 . Cosign(and various clients) should be changed to use Pure Ed25519 when Ed25519ph (or other Ed25519 variants) is used and it interacts with Fulcio.

Comment on lines +43 to +59
func getHashAlgorithmFromSignerVerifier(sv *SignerVerifier) (crypto.Hash, error) {
publicKey, err := sv.SignerVerifier.PublicKey()
if err != nil {
return crypto.Hash(0), err
}

switch publicKey.(type) {
case *ecdsa.PublicKey:
return crypto.SHA256, nil
case *rsa.PublicKey:
return crypto.SHA256, nil
case ed25519.PublicKey:
return crypto.SHA512, nil
default:
return crypto.Hash(0), fmt.Errorf("unsupported public key type")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you be interested in taking a look at the failing test in sigstore/sigstore#1426? I think it's just a failure due to a mock not having an expectation met. We could get that merged in then to use here.


switch publicKey.(type) {
case *ecdsa.PublicKey:
return crypto.SHA256, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe for a later PR to keep this smaller in scope - Should this be based on curve too? This would be a change from current behavior, since I believe we're using sha256 regardless of curve. This might be an unexpected change for custom verifiers though who likely have always assumed sha256.

Copy link
Author

Choose a reason for hiding this comment

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

Yes I agree, there should be more flexibility here.

cmd/cosign/cli/sign/sign.go Outdated Show resolved Hide resolved
@@ -214,11 +215,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
}
}

svOpts := []signature.LoadOption{
signatureoptions.WithHash(crypto.SHA256),
signatureoptions.WithED25519ph(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need the trial verification you mentioned in the comment, verifying both as ed25519 and ed25519ph?

Copy link
Author

Choose a reason for hiding this comment

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

At this stage, yes, I think that's needed.

However, once we add support for --signing-algorithm flag, the user can specific the algorithm he wants and we can add or not this option.

See https://github.com/sigstore/cosign/pull/3497/files#diff-fc9a26a4caa1d8684a4bdd9102f187f44e1f7fce7c06a19f4185035f13317cc8R232-R238 .

Copy link
Author

Choose a reason for hiding this comment

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

I realize now that you might be saying there is no trial verification in the PR, but this is already implemented in https://github.com/sigstore/cosign/pull/3479/files#diff-8a85c8e688d61e16b8af8e09832ed2bef89c1163b0e9601a8363c782c387c006R272-R298 . Was that what you were referring to or did I misinterpret your messages?

sv, err := SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko)
svOptions := []signature.LoadOption{
signatureoptions.WithHash(crypto.SHA256),
signatureoptions.WithED25519ph(),
Copy link
Contributor

Choose a reason for hiding this comment

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

To confirm, this would mean that self-managed ed25519 keys would now generate signatures with the pre-hashed variant, correct? This would mean that a verifier using an older Cosign version could not verify a signature from the latest Cosign version. Could we set this only when tlogupload (or the equivalent config) is true? This preserves the same signing behavior then for self-managed keys with sigs not being uploaded to Rekor.

Copy link
Author

Choose a reason for hiding this comment

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

Implemented!

@haydentherapper
Copy link
Contributor

I believe this is missing the trial verification and the last comment about limiting when the key is the prehash variant?

* Use ED25519ph algorithm with sign/verify-blob commands

Signed-off-by: Riccardo Schirone <riccardo.schirone@trailofbits.com>
Signed-off-by: William Woodruff <william@trailofbits.com>
Signed-off-by: Riccardo Schirone <riccardo.schirone@trailofbits.com>
ED25519-ph is not widely supported and it is not an accepted option in
x509 Certificates/CSR, so Fulcio does not accept them. Instead, clients
are supposed to use PureED25519 when interacting with Fulcio.

This commit provides to the Fulcio code a separate SignerVerifier
created from the one loaded from the private key. This SignerVerifier is
usually of the same type, except when dealing with ED25519ph.

Signed-off-by: Riccardo Schirone <riccardo.schirone@trailofbits.com>
Signed-off-by: Riccardo Schirone <562321+ret2libc@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants