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

Fixes JWS signing bug where JSON unmarshalling breaks verification #105

Merged
merged 7 commits into from
Mar 5, 2024

Conversation

EthanHeilman
Copy link
Member

@EthanHeilman EthanHeilman commented Feb 26, 2024

This fixes an bug where marshalling and unmarshalling the PK Token could break verification of the PK Token in semi-rare circumstances.

Bug Description

PR #45 Use jws signatures introduced a bug that in some circumstances break signature verification.

JSON Marshalling/Unmarshalling json result in a different json representation. For instance the json string {"c": 1, "b": 2, "a": 3} could be marshalled and unmarshalled to json string (with alphabetized keys): {"a": 3, "b": 2, "c": 1}
as order of keys is not guaranteed to be preserved during marshaling. A signature over {"c": 1, "b": 2, "a": 3} will not verify for {"a": 3, "b": 2, "c": 1} even if semantically they are same object. This issue is not limited to only alphabetization, a similar problem exists for whitespace.

Prior to PR #45 we stored the original signed bytes of the OP, CIC and COS in the PK Token to ensure that the bytes we signed are the same bytes we used to construct the marshalled version of the PK Token for verification.

type PKToken struct {
	Payload []byte
	OpPH    []byte
	OpSig   []byte
...

PR #45 altered this behavior so that the bytes we signed were unmarshalled into a jws.signature object.

type Signature = jws.Signature

type PKToken struct {
	raw []byte // the original, raw representation of the object

	Payload []byte     // decoded payload
	Op      *Signature // Provider Signature
	Cic     *Signature // Client Signature
	Cos     *Signature // Cosigner Signature

This did not break verification for our current OP and it was only until experimenting with Azure that this bug was discovered.

Fix

We fix this bug storing the signed tokens in the PK Token.

OpToken  []byte // Base64 encoded ID Token signed by the OP
CicToken []byte // Base64 encoded Token signed by the Client
CosToken []byte // Base64 encoded Token signed by the Cosigner

Then we plug these bytes for the fields in the json PK Token so that you always verify the exact bytes that were signed.

Additionally we needed to change the pkt.Compact function. pkt.Compact takes an unmarshalled signature and then produces the a compact token for verification. This assumes that the unmarshalled signature is safe to use for verificatio but there are several circumstances in which the returned compact token my not verify. To keep the lines of code in this PR low, this PR does not fully remove pkt.Compact. Instead it changes pkt.Compact` to simply return the saved compact token in the PK Token making it safe to use. We have marked FromCompact as depreciated and plan to remove it completely in a future PR.

Instead of pkt.Compact, developers should simply use the compact token saved to the PK Token.
Rather than:

cicToken, err := pkt.Compact(pkt.Cic)

do this instead:

cicToken := pkt.CicToken

In a future PR pkt.Compact will be removed.

Regression Tests

We also created a regression test which creates a PK Token raw json strings and then we marshal and unmarshal the PK Token and check if the signed values are still equal to the original signed values.

I tested this PR against the Google and MFA example tests on windows.

TODOs:

  • Change unittests use a table test
  • Fix strange memory test failures that seem to happen randomly after this code change

@EthanHeilman EthanHeilman added the bug Something isn't working label Feb 26, 2024
@EthanHeilman EthanHeilman self-assigned this Feb 26, 2024
Copy link
Member

@mrjoelkamp mrjoelkamp left a comment

Choose a reason for hiding this comment

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

LGTM, just one thing in simplejws.go

pktoken/simplejws/simplejws.go Outdated Show resolved Hide resolved
@EthanHeilman EthanHeilman changed the title Fixes JWS signing bug where we JSON unmarshalling breaks verification Fixes JWS signing bug where JSON unmarshalling breaks verification Feb 29, 2024
Co-authored-by: Joel Kamp <mrjoelkamp@gmail.com>
Signed-off-by: Ethan Heilman <ethan.r.heilman@gmail.com>
@@ -175,11 +184,22 @@ func (p *PKToken) ProviderAlgorithm() (jwa.SignatureAlgorithm, bool) {
return alg.(jwa.SignatureAlgorithm), true
}

// Deprecated: The PK Token now stores the signed tokens such as OpToken,
Copy link
Member

Choose a reason for hiding this comment

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

If this PR is deprecating a function, it seems like we should be replacing everywhere we internally use that function in this PR, otherwise I'd doubt it would ever be done and therefore we would never be able to remove the function

Copy link
Member Author

Choose a reason for hiding this comment

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

Next PR after this one removes Compact. I wanted to keep this PR small and fast to review so I'm adding the bugfix here and them removing it later.

I created an issue to track this #107


if p.Cos != nil {
message.AppendSignature(p.Cos)
rawJws := simplejws.Jws{
Copy link
Member

Choose a reason for hiding this comment

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

if we're creating out own types here that are safe from the issues with the jws library we were using, then why do we need to also save the raw tokens as values when we can replace the jws.Signature with our own and always be able to create them from the object as saved?

Copy link
Member Author

Choose a reason for hiding this comment

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

Eventually I hope to remove all dependencies on jwx. That is too large of a change to make in a single PR and impacts too much. This PR introduces simplejws as the acorn to fix the bug and set that feature work up.
The general sketch of this work is:

  1. We move to compact tokens internally in the pktoken (this PR)
  2. Then we remove the Compact function from pktoken
  3. Hopefully by this point we have moved CIC creation and verification into the providers.
  4. Slow start moving away from jwx

@EthanHeilman
Copy link
Member Author

@lgmugnier Any objection to merging?

@EthanHeilman
Copy link
Member Author

Lucie is good with merging this

@EthanHeilman EthanHeilman merged commit 62604f0 into openpubkey:main Mar 5, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants