5

Edit: I found the answer. Scroll to the bottom of this question.

I am working on a NodeJS authentication server and I would like to sign JSON Web Tokens (JWT) using google signatures.

I am using Google Cloud Key Management Service (KMS) and I created a key ring and an asymmetric signing key.

This is my code to get the signature:

signatureObject = await client.asymmetricSign({ name, digest })

signature = signatureObject["0"].signature

My Google signature object looks like this:

enter image description here

My question: How do I sign a JWT using the Google signature?

Or in other words, how do I concatenate the Google signature to the (header.payload) of the JWT?

The JWT should look something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. (GoogleSignature)

The Code I am using:

signing:

async function sign(message, name) {
  hashedMessage = crypto.createHash('sha256').update(message).digest('base64');
  digest = { 'sha256': hashedMessage }

  signatureObject = await client.asymmetricSign({ name, digest }).catch((err) => console.log(err))
  signature = signatureObject["0"].signature
  signJWT(signature)
}

Creating the JWT:

function signJWT(signature) {
  header = {
    alg: "RS256",
    typ: "JWT"
  }

  payload = {
    sub: "1234567890",
    name: "John Doe",
    iat: 1516239022
  }

  JWT = base64url(JSON.stringify(header)) + "." +
        base64url(JSON.stringify(payload)) + "." + 
        ???signature??? ; // what goes here?
}

Verifying:

async function validateSignature(message, signature) {
  // Get public key
  publicKeyObject = await client.getPublicKey({ name }).catch((err) => console.log(err))
  publicKey = publicKeyObject["0"].pem

  //Verify signature
  var verifier = crypto.createVerify('sha256');
  verifier.update(message)
  var ver = verifier.verify(publicKey, signature, 'base64')

  // Returns either true for a valid signature, or false for not valid.
  return ver
}

The Answer:

I can use the toString() method like so:

signatureString = signature.toString('base64');

AND then I can get the original signature octet stream by using

var buffer = Buffer.from(theString, 'base64');
  • What library are you using for signing? Have you considered using a JWT library? – John Hanley Jan 18 '19 at 18:15
  • @JohnHanley I don't use a library for signing the JWT since there is no JWT library that supports Google signatures. – Jim van Lienden Jan 19 '19 at 12:52
  • Can you explain what you mean by "Google Signatures"? – John Hanley Jan 19 '19 at 18:26
  • @JohnHanley With "Google Signatures" I mean signatures made by Google KMS with my asymmetric signing key, as explained in the question above. https://cloud.google.com/kms/docs/digital-signatures – Jim van Lienden Jan 19 '19 at 19:46
  • I updated my answer with pseudo code to show you how to sign the JWT based from your code. – John Hanley Jan 19 '19 at 20:08
  • @JohnHanley Thank you for your help again, unfortunately I already did what you suggested in the edit. The problem lies in: **_"Note: I am not sure what data format the signature is returned by signatureObject["0"].signature."_** I have no clue how to convert the object returned by "signatureObject["0"].signature" to a string. This object seems to be a Buffer(256). I can't get my hands on the code this weekend, I will post more detailed images of the object on monday. – Jim van Lienden Jan 19 '19 at 21:45
  • Try this `signature = sign(body_b64, name); var sig_b64 = signture.toString('base64'); jwt = body_b64 + '.' + sig_b64;` – John Hanley Jan 19 '19 at 23:52
  • @JohnHanley I've tried that, and that works for creating the signature. The problem with that approach is that I can't verify the signature like that. I don't think there is a way to go back from the _"signature.toString('base64')"_ to the actual signature. – Jim van Lienden Jan 20 '19 at 11:03
  • @JimvanLienden can you please share an updated working code, I tried using your solution, seems like I'm missing something. Updated working code would be a great help. – Niket Malik Apr 24 '19 at 12:41
  • @NiketMalik Tell me where you're stuck. The code works for me. – Jim van Lienden Apr 26 '19 at 08:02
  • 1
    @JimvanLienden thanks for the help, I got my answer at https://stackoverflow.com/questions/55828435/google-cloud-key-management-service-to-sign-json-web-tokens – Niket Malik Apr 26 '19 at 10:05

1 Answers1

2

You did not post your code in your question, so I do not know how you are building the JWT for signing.

[EDIT 1/18/2019 after code added to question]

Your code is doing the signature backwards. You are creating a signature and trying to attach it to the JWT Headers + Payload. You want to instead take the JWT Headers + Payload and sign that data and then attach the signature to the JWT to create a Signed-JWT.

Psuedo code using your source code:

body_b64 = base64url(JSON.stringify(header)) + "." + base64url(JSON.stringify(payload))

signature = sign(body_b64, name);

jwt = body_b64 + '.' + base64url(signature)

Note: I am not sure what data format the signature is returned by signatureObject["0"].signature. You may have to convert this before converting to base64.

[END EDIT]

Example data:

JWT Header:

{
    alg: RS256
    kid: 0123456789abcdef62afcbbf01234567890abcdef
    typ: JWT
}

JWT Payload:

{
  "azp": "123456789012-gooddogsgotoheaven.apps.googleusercontent.com",
  "aud": "123456789012-gooddogsgotoheaven.apps.googleusercontent.com",
  "sub": "123456789012345678901",
  "scope": "https://www.googleapis.com/auth/cloud-platform",
  "exp": "1547806224",
  "expires_in": "3596",
  "email": "someone@example.com.com",
  "email_verified": "true",
  "access_type": "offline"
}

Algorithm:

SHA256withRSA

To create a Signed JWT (JWS):

Step 1: Take the JWT Header and convert to Base-64. Let's call this hdr_b64.

Step 2: Take the JWT Payload and convert to Base-64. Let's call this payload_b64.

Step 3: Concatenate the encoded header and payload with a dot . in between: hdr_b64 + '.' + payload_b64`. Let's call this body_b64.

Step 4: Normally a JWS is signed with SHA256withRSA often called "RS256" using the Private Key:

signature = sign(body_b64, RS256, private_key)

Now convert the signature to Base-64. Let call this signature_b64.

To create the final JWS:

jws = body_b64 + '.' + signature_b64.

Recommendations:

Do you want to use KMS to create Signed JWTs? I would not recommend this. There is a cost accessing keys stored in KMS. Signed-JWTs are signed with the private key and verified with the public key. How are you going to publish the public key? What performance level do you need in accessing the private and public keys (how often will you be signing and verifying)?

When you create a service account in Google Cloud Platform, a keypair is created for you. This keypair has an ID with the public key available on the Internet and the private key is present in the Service Account Json credentials file. I would use a Service Account to create Signed-JWTs instead of a keypair in KMS.

Example code in Python to create and sign:

def create_signed_jwt(pkey, pkey_id, email, scope):
    '''
    Create a Signed JWT from a service account Json credentials file
    This Signed JWT will later be exchanged for an Access Token
   '''

    import jwt

    # Google Endpoint for creating OAuth 2.0 Access Tokens from Signed-JWT
    auth_url = "https://www.googleapis.com/oauth2/v4/token"

    issued = int(time.time())
    expires = issued + expires_in   # expires_in is in seconds

    # Note: this token expires and cannot be refreshed. The token must be recreated

    # JWT Headers
    headers = {
        "kid": pkey_id, # This is the service account private key ID
        "alg": "RS256",
        "typ": "JWT"    # Google uses SHA256withRSA
    }

    # JWT Payload
    payload = {
            "iss": email,           # Issuer claim
            "sub": email,           # Issuer claim
            "aud": auth_url,        # Audience claim
            "iat": issued,          # Issued At claim
            "exp": expires,         # Expire time
            "scope": scope          # Permissions
    }

    # Encode the headers and payload and sign creating a Signed JWT (JWS)
    sig = jwt.encode(payload, pkey, algorithm="RS256", headers=headers)

    return sig
John Hanley
  • 74,467
  • 6
  • 95
  • 159
  • KMS publishes public keys. Your recommendation does not make sense. You suggest downloading private key for a service account and... then what? Hand it to your developers? KMS is designed exactly for this case. – Nikola Mihajlović May 09 '21 at 04:34
  • @NikolaMihajlović - And how will the developers access KMS? Your developers will need credentials to access KMS which means a service account or user credentials. If my recommendations do not make sense to you, create a new question and reference this answer. And no, KMS cannot create service account credentials. KMS can store service account JSON keys, which then require another credential (service account or user) to access them. – John Hanley May 09 '21 at 05:41
  • A service account should be used to access KMS. Developers don't need access to KMS, only production does. Your suggestion to use a service account to sign tokens makes sense in dev, but not in prod. – Nikola Mihajlović May 09 '21 at 17:59
  • @NikolaMihajlović - Wow, that is an interesting perspective. Post another answer. – John Hanley May 09 '21 at 18:01
  • Agreed, it's only about a comment about feasibility of KMS to sign JWTs. I believe it is — the cost is $0.03 per 10k signatures. If you expect 1M logins in a month it comes out to $3. But by using KMS no private keys are exposed anywhere for production, there is no need to generate a private key for a service account at all. So I can rest assured that no employee can steal a private key when they get fired. – Nikola Mihajlović May 09 '21 at 18:10
  • @NikolaMihajlović - If only Google Cloud services are accessing other Google Cloud services, then the service account private key is not needed by developers or production systems. Google has designed service accounts into most services. Therefore, you do not even need KMS to store keys. Then there is delegation which allows an identity to impersonate a service account. – John Hanley May 09 '21 at 18:19