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 initial support for GQ signatures #3

Merged
merged 4 commits into from Sep 20, 2023

Conversation

jonnystoten
Copy link
Member

@jonnystoten jonnystoten commented Sep 20, 2023

Closes #8

Adds a gq package for GQ signature support. The package stands alone at the moment, it isn't actually used by any of the other code yet.

Currently, an OpenPubkey token contains the original signature from the OIDC provider. This arguably means that it isn't safe to share the OpenPubkey token, as the token could be used to impersonate the subject to a service that doesn't check the audience claim, or to trick an OpenPubkey client to issue a token for the subject.

We can create a GQ signature to prove that we have the original OIDC provider's signature. This can be verified using the original OIDC signing payload (header || '.' || payload) and the OIDC provider's public key. This works because GQ signature private keys are equivalent to RSA signatures.

The algorithms described here are from GQ1 in ISO/IEC 14888-2:2008.

Notation

If $n$ is a number, $|n|$ is the number of bits required to store $n$.

Signing flow

Input

$M$: message to sign
$v$, $n$: public exponent and modulus from OIDC provider's public key
$x$: RSA signature from OIDC ID token
$t$: signature length parameter (number of (challenge, response) pairs below), to achieve $\lambda$-bit soundness this should be set to $\lambda / (|v|-1)$.

Algorithm

  1. Calculate $Q \gets x^{-1} \mod n$
  2. Select $t$ numbers, $r_1, r_2, \dots, r_t$, each consisting of $|n|$ random bits
  3. For $i$ from 1 to $t$, calculate $W_i \gets r_i^v \mod n$, combine to form $W$
  4. Hash $W||M$ and take the first $t \times (|v|-1)$ bits as $R$, split $R$ into $t$ parts
  5. For $i$ from 1 to $t$, calculate $S_i \gets r_i \times Q^{R_i} \mod n$, combine to form $S$
  6. Signature is $(R, S)$

Verification flow

Input

$M$: signed message
$R, S$: signature
$v$, $n$: public exponent and modulus from OIDC provider's public key
$Id$: Signing payload for RSA signature $x$ given as input to signing flow (OIDC ID token header/payload)
$t$: signature length parameter, must match $t$ for signing flow

Algorithm

  1. Reject if $|R| \neq t \times (|v|-1)$ or $|S| \neq t \times |n|$
  2. Calculate $G \gets \text{format}(Id)$ (for RSA-signed JWT this should be PKCS#1 v1.5)
  3. Split $R$ and $S$ into $t$ parts. For $i$ from 1 to $t$, calculate $W^*_i \gets S_i^v \times G^{R_i} \mod n$, combine to form $W^*$. Reject if any $S_i = 0$ or $S_i \geq n$.
  4. Hash $W^*||M$ and take the first $t(|v|-1)$ bits as $R^*$, split $R^*$ into $t$ parts
  5. Accept if $R$ and $R^*$ are identical, otherwise reject

Potential issues

Go's math/big package says:

Note that methods may leak the Int's value through timing side-channels. Because of this and because of the scope and complexity of the implementation, Int is not well-suited to implement cryptographic operations. The standard library avoids exposing non-trivial Int methods to attacker-controlled inputs and the determination of whether a bug in math/big is considered a security vulnerability might depend on the impact on the standard library.

I'm not sure if this is a problem at the moment.

We currently just pass the OIDC payload as the message to be signed. This could be something more useful. I think it's fine to basically sign the public key?

Using sign rather than prove as that's what we're really doing with GQ
@EthanHeilman
Copy link
Member

Just working through your protocol description. One thing that confused me is that you use $t$ to be the desired length of GQ signature, but you also use it to represent the number of (challenge, response) pairs. Perhaps consider defining $t$ as

$t$: signature length parameter, to achieve $\lambda$-bit soundness this should be $|\lambda|-1 \ge t*|v|$

@jonnystoten
Copy link
Member Author

Yes, what I've written as the suggested value of $t$ is not at all right. By length here I do mean number of (challenge, response) pairs, I'll make that clearer. What I meant to suggest for the value of $t$ was $\lambda / (|v| - 1)$. At the moment the code takes $\lambda$ as a parameter and calculates $t$ in that way.

@EthanHeilman
Copy link
Member

Timing side challenges in RSA are real and something we should be concerned with and even if we were using bigint, we'd want to have unittests that confirmed that there operations are constant time. I don't think this should hold up merging the PR, instead I've created an issue so that we can do a full review later: #11

Copy link
Member

@EthanHeilman EthanHeilman left a comment

Choose a reason for hiding this comment

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

Did a light code review, deferring a full crypto review until later (see #11). Happy to merge if after math/rand fix. Beyond that, it looks good

gq/gq_test.go Outdated
"testing"
"time"

insecurerand "math/rand"
Copy link
Member

Choose a reason for hiding this comment

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

Could you use: "crypto/rand" here instead or is there a dependency on the RSA library?

See example of crypto/rand usage:
https://github.com/bastionzero/openpubkey/blob/main/pktoken/signer.go#L186

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yeah, I'm using math/rand for this test because crypto/rand doesn't support seeding the rng (as you would hope) and I wanted the tests to be deterministic. I realize I've only half-implemented what I was planning though - to seed the rng with a random number which we also print out, so that if there's some failure in a test to do with the random input we can reproduce it easily.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh never mind, this doesn't actually work - I thought it was because I was looking at cached test output. The rsa.GenerateKey function has some code that randomly reads an extra byte from the rng to stop you relying on it being deterministic with a seeded rng 😅

I will remove the parameters I've added and just use crypto/rand.Reader everywhere.

Copy link
Member

Choose a reason for hiding this comment

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

Apologizes, that's my mistake. I thought that was the RNG for gq, not the test, the file name gq_test.go should have tipped me off.

I realize I've only half-implemented what I was planning though - to seed the rng with a random number which we also print out, so that if there's some failure in a test to do with the random input we can reproduce it easily.

That's an excellent idea. If you figure out a way to implement it with rsa.GenerateKey, we should do it. If not, don't worry about it.

@EthanHeilman EthanHeilman merged commit ba7e657 into openpubkey:main Sep 20, 2023
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.

GQ signer/verifier
2 participants