Is Sigstore Susceptible to Psychic Signatures? Sources Say: Sounds Suspect

Is Sigstore Susceptible to Psychic Signatures? Sources Say: Sounds Suspect

The internet has been abuzz this week with talk of the so-called “psychic signature” vulnerability in Java. This vulnerability allows an attacker to easily bypass security checks requiring ECDSA signatures. Neil Madden, who discovered this vulnerability, compares this vulnerability to Doctor Who’s “psychic paper,” blank paper that tricks others into seeing valid credentials.

As part of the Sigstore community, we wanted to know: Does the psychic signature vulnerability also affect Sigstore, a code artifact signing and verification project?

The short answer is NO!

The Sigstore infrastructure and Cosign are written in Go.[1] The Go cryptography libraries were not affected by this bug, and furthermore Rekor, the Sigstore transparency log, rejects attempted psychic signatures. In fact, Sigstore actually helps check whether attackers have attempted to exploit bugs like this! The rest of this post illustrates how Sigstore rejects physic signatures and exposes an attacker that attempts to exploit this vulnerability.


  1. There's no official Java implementation of Sigstore, though there's a work-in-progress effort. But even a Java implementation of a Sigstore client wouldn't have been vulnerable if it checked for signatures in Rekor, which rejects psychic signatures. ↩︎

Setting the Scene: Sigstore and Psychic Signatures

Sigstore. Sigstore is an OpenSSF project that aims to make signing artifacts (especially the artifacts in the software supply chain) as easy as logging in to your email account—literally. It provides a tool, Cosign, which allows developers to sign container images and other artifacts without managing signing keys. Instead, users use OIDC to log in to the Sigstore infrastructure, which securely associates a temporary signing key with their identity (for instance, their GitHub, Google, or Microsoft account). The component of Sigstore that we’ll be talking about the most today is Rekor, which stores every signature that’s considered valid in the Sigstore ecosystem in a transparency log. To validate a Sigstore signature, you check that the signature is valid, and that it is stored in Rekor.

Because signatures are an essential part of Sigstore, you might worry that the psychic signature vulnerability affects it. To see why this is not the case, let’s first understand at a high level what signatures are and what the vulnerability is.

Digital signatures. When we say “signatures,” we’re referring to digital signatures using asymmetric cryptography. If Alice wants to sign a message, she generates a “key pair” comprising public and private keys. The private key can be used to “sign” the message, producing a “signature.” Bob can then verify this signature using the public key.

ECDSA and psychic signatures. One common implementation of digital signatures is called ECDSA. This uses some mathematical tricks involving arithmetic on an elliptic curve for signing and verification. In particular, an ECDSA signature is two integers, usually called r and s. The verifier checks an equation involving r, s, the public key, and the message. Implementations are supposed to check that r and s are nonzero. However, the Java implementation of ECDSA signature verification skipped this check. See the blog post announcing the vulnerability for a clear in-depth explanation.

Sigstore Shirks Psychic Signatures

Let’s first demonstrate that Rekor rejects psychic signatures. (The full code for this blog post is available on GitHub.) We can make a psychic signature in ASN.1 DER format (which Sigstore uses under-the-hood):

var b cryptobyte.Builder
	b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
		b.AddASN1BigInt(big.NewInt(0))
		b.AddASN1BigInt(big.NewInt(0))
	})
sig, _ := b.Bytes()
cmd/uploadbadsig/main.go:BadSig()

Then, we can use Cosign to attempt to upload a HashedRekord to Rekor:

payload := make([]byte, 0)
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
pubKeyPEM, _ := cryptoutils.MarshalPublicKeyToPEM(key.Public())
client := rekor.Default
logEntry, err := cosign.TLogUpload(ctx, client, sig, payload, pubKeyPEM)
if err != nil {
	log.Fatalf("Error uploading psychic signature to tlog: %v", err)
}
cmd/uploadbadsig/main.go:main()

When we run this, we get an error:

2022/04/22 07:54:55 Error uploading to tlog: [POST /api/v1/log/entries][400] createLogEntryBadRequest &{Code:400 Message:Error processing entry: verifying signature: failed to verify signature}

Rekor doesn’t verify the signature because it isn’t vulnerable to this bug. This means that even a vulnerable implementation would reject these signatures, because they aren’t in the log!

Sigstore Surfaces Suspected Shenanigans

Even if Sigstore had been vulnerable to this bug, Rekor makes it simple to scan for attempted exploitation: at any time, we can enumerate all Sigstore signatures up to that point and check whether they’re affected.

Now imagine that the Go implementation of ECDSA signature verification was actually vulnerable to psychic signatures. While the Sigstore team would have patched the main Rekor deployment immediately, you might still wonder if anyone had ever previously attempted to exploit this bug.

We can easily check that! Start with a CSV file containing all Rekor signatures (reach out in the Sigstore Slack if you’re interested in how we got this). In our example format, the file contains one line per Rekor entry: its UUID, its index in the Rekor log, and the entry’s signature:

$ head -n 3 signatures.csv
logIndex,uuid,body.spec.signature.content
1,073970a[snip uuid...]d0bd95c0,MAYCAQACAQA=
2,06f7fa3[snip uuid...]099ddab5,MEYCIQDiFwPAGq[snip sig...]+z3IdS/8QYk
signatures.csv

We can parse each signature, which is a base64 encoding of r and s (the ECDSA signature) in ASN.1 DER format:

func ParseECDSASignature(sigString string) (*big.Int, *big.Int, error) {   
	var (
		r, s  = &big.Int{}, &big.Int{}
		inner cryptobyte.String
	)
	sig, _ := base64.StdEncoding.DecodeString(sigString)
	input := cryptobyte.String(sig)
	input.ReadASN1(&inner, asn1.SEQUENCE)
	inner.ReadASN1Integer(r)
	inner.ReadASN1Integer(s)
	return r, s, nil // full code gives error on invalid format
}
cmd/csvcheck/main.go:ParseECDSASignature()

Then, looking for psychic signatures is straightforward:

r := csv.NewReader(os.Stdin)
for {
	record, err := r.Read() // record: logIndex,uuid,signature
	if err == io.EOF {
		break
	}
	sig := record[2]
	r, s, err := ParseECDSASignature(sig)
	if err != nil {
		continue // bad ECDSA signature; maybe in another format
	}
	zero := big.NewInt(0)
	if r.Cmp(zero) == 0 && s.Cmp(zero) == 0 {
		log.Printf("Found psychic signature!")
	}
}
cmd/csvcheck/main.go:main()

Let’s try it out on the CSV file from before:

$ cat signatures.csv | go run ./cmd/csvcheck
2022/04/22 08:35:23 Found psychic signature!

Wait—what’s up with “Found psychic signature?” To make this exercise a little more interesting, I added a fake “psychic signature” to the CSV (base64- and DER-encoded, it looks like MAYCAQACAQA=). This doesn’t turn up in the real log, because (as we saw above) Rekor rejects it. If we run this on the actual data[1], we find no psychic signatures, as expected:

$ cat signatures-real.csv | go run ./cmd/csvcheck
$

  1. We haven't made signatures-real.csv available because it's quite large. If you're interested in doing large-scale analysis of Rekor data, reach out in the Sigstore Slack. ↩︎

Conclusion

Sigstore is not vulnerable to the “psychic signature bug.” In fact, using Sigstore can help detect exploitation of similar bugs. Finally, because Sigstore rejects psychic signatures, even a vulnerable Java implementation wouldn’t have been affected by the psychic signature bug if it also checked that signatures were present in Rekor.

Show Comments