Constructing and validating a Gigya signature

狂风中的少年 提交于 2019-12-03 02:54:50

I would take a close look at your Base-64 encoding and decoding.

Are you using a third-party library for this? If so, which one? If not, can you post your own implementation or at least some sample input and output (representing bytes with hexadecimal)?

Sometimes there are differences in the "extra" Base-64 characters that are used (substituting characters for '/' and '+'). Padding can also be omitted, which would cause string comparison to fail.


As I suspected, it is the Base-64 encoding that is causing this discrepancy. However, it is trailing whitespace that is causing the problem, not differences in padding or symbols.

The encodeBase64String() method that you are using always appends CRLF to its output. The Gigya signature does not include this trailing whitespace. Comparing these strings for equality fails only because of this difference in whitespace.

Use encodeBase64String() from the Commons Codec library (instead of Commons Net) to create a valid signature.

If we factor out the signature computation, and test its result against the Gigya SDK's verifier, we can see that removing the CRLF creates a valid signature:

public static void main(String... argv)
  throws Exception
{
  final String u = "";
  final String t = "";
  final String s = MyConfig.getGigyaSecretKey();

  final String signature = sign(u, t, s);
  System.out.print("Original valid? ");
  /* This prints "false" */
  System.out.println(SigUtils.validateUserSignature(u, t, s, signature));

  final String stripped = signature.replaceAll("\r\n$", "");
  System.out.print("Stripped valid? ");
  /* This prints "true" */
  System.out.println(SigUtils.validateUserSignature(u, t, s, stripped));
}

/* This is the original computation included in the question. */
static String sign(String uid, String timestamp, String key)
  throws Exception
{
  String baseString = timestamp + "_" + uid;
  byte[] baseBytes = baseString.getBytes("UTF-8");
  byte[] secretKeyBytes = Base64.decodeBase64(key);
  Mac mac = Mac.getInstance("HmacSHA1");
  mac.init(new SecretKeySpec(secretKeyBytes, "HmacSHA1"));
  byte[] signatureBytes = mac.doFinal(baseBytes);
  return Base64.encodeBase64String(signatureBytes);
}
MrGomez

Code review time! I love doing these. Let's check your solution and see where we fall.

In prose, our goal is to underscore-connect a timestamp and UID together, coerce the result from UTF-8 into a byte array, coerce a given Base64 secret key to into a second byte array, SHA-1 the two byte arrays together, then convert the result back to Base64. Simple, right?

(Yes, that pseudocode has a bug.)

Let's step through your code, now:

public boolean verifyGigyaSig(String uid, String timestamp, String signature) {

Your method signature here is fine. Though obviously, you'll want to make sure your created timestamps and the ones you're verifying are using the exact same format (otherwise, this will always fail) and that your Strings are UTF-8 encoded.

(Further details about how String encodings work in Java)

    // Construct the "base string"
    String baseString = timestamp + "_" + uid;

    // Convert the base string into a binary array
    byte[] baseBytes = baseString.getBytes("UTF-8");

This is fine (reference a, reference b). But, in the future, consider using StringBuilder for String concatenation explicitly, instead of relying on compiler-time optimizations to support this feature.

Note the documentation up to this point is inconsistent on whether to use "UTF-8" or "UTF8" as your charset identifier. "UTF-8" is the accepted identifier, though; I believe "UTF8" is kept for legacy and compatibility purposes.

    // Convert secretKey from BASE64 to a binary array
    String secretKey = MyConfig.getGigyaSecretKey();
    byte[] secretKeyBytes = Base64.decodeBase64(secretKey);

Hold it! This breaks encapsulation. It's functionally correct, but it would be better if you passed this as a parameter to your method than pulling it in from another source (thus coupling your code, in this case, to the details of MyConfig). Otherwise, this, too, is fine.

    // Use the HMAC-SHA1 algorithm to calculate the signature 
    Mac mac = Mac.getInstance("HmacSHA1");
    mac.init(new SecretKeySpec(secretKeyBytes, "HmacSHA1"));
    byte[] signatureBytes = mac.doFinal(baseBytes);

Yep, this is correct (reference a, reference b, reference c). I've nothing to add here.

    // Convert the signature to a BASE64
    String calculatedSignature = Base64.encodeBase64String(signatureBytes);

Correct, and...

    // Return true iff constructed signature equals specified signature
    return signature.equals(calculatedSignature);
}

... correct. Ignoring the caveats and implementation notes, your code checks out procedurally.

I would speculate on a few points, though:

  1. Are you UTF-8 encoding your input String for your UID or your timestamp, as defined here? If you've failed to do so, you're not going to get the results you expect!

  2. Are you sure the secret key is correct and properly encoded? Make sure to check this in a debugger!

  3. For that matter, verify the whole thing in a debugger if you have access to a signature generation algorithm, in Java or otherwise. Failing this, synthesizing one will help you check your work because of the encoding caveats raised in the documentation.

The pseudocode bug should be reported, as well.

I believe checking your work here, especially your String encodings, will expose the correct solution.


Edit:

I checked their implementation of Base64 against Apache Commons Codec's. Test code:

import org.apache.commons.codec.binary.Base64;
import static com.gigya.socialize.Base64.*;

import java.io.IOException;

public class CompareBase64 {
    public static void main(String[] args) 
      throws IOException, ClassNotFoundException {
        byte[] test = "This is a test string.".getBytes();
        String a = Base64.encodeBase64String(test);
        String b = encodeToString(test, false);
        byte[] c = Base64.decodeBase64(a);
        byte[] d = decode(b);
        assert(a.equals(b));
        for (int i = 0; i < c.length; ++i) {
            assert(c[i] == d[i]);
        }
        assert(Base64.encodeBase64String(c).equals(encodeToString(d, false)));
        System.out.println(a);
        System.out.println(b);
    }
}

Simple tests show that their output is comparable. Output:

dGhpcyBpcyBteSB0ZXN0IHN0cmluZw==
dGhpcyBpcyBteSB0ZXN0IHN0cmluZw==

I verified this in a debugger, just in case there might be whitespace I can't detect in visual analysis and the assert didn't hit. They're identical. I also checked a paragraph of lorem ipsum, just to be sure.

Here's the source code for their signature generator, sans Javadoc (author credit: Raviv Pavel):

public static boolean validateUserSignature(String UID, String timestamp, String secret, String signature) throws InvalidKeyException, UnsupportedEncodingException
{
    String expectedSig = calcSignature("HmacSHA1", timestamp+"_"+UID, Base64.decode(secret)); 
    return expectedSig.equals(signature);   
}

private static String calcSignature(String algorithmName, String text, byte[] key) throws InvalidKeyException, UnsupportedEncodingException  
{
    byte[] textData  = text.getBytes("UTF-8");
    SecretKeySpec signingKey = new SecretKeySpec(key, algorithmName);

    Mac mac;
    try {
        mac = Mac.getInstance(algorithmName);
    } catch (NoSuchAlgorithmException e) {
        return null;
    }

    mac.init(signingKey);
    byte[] rawHmac = mac.doFinal(textData);

    return Base64.encodeToString(rawHmac, false);           
}

Changing your function signature in line with some of the changes I made above and running this test case causes both signatures to be validated correctly:

// Redefined your method signature as: 
//  public static boolean verifyGigyaSig(
//      String uid, String timestamp, String secret, String signature)

public static void main(String[] args) throws 
  IOException,ClassNotFoundException,InvalidKeyException,
  NoSuchAlgorithmException,UnsupportedEncodingException {

    String uid = "10242048";
    String timestamp = "imagine this is a timestamp";
    String secret = "sosecure";

    String signature = calcSignature("HmacSHA1", 
              timestamp+"_"+uid, secret.getBytes());
    boolean yours = verifyGigyaSig(
              uid,timestamp,encodeToString(secret.getBytes(),false),signature);
    boolean theirs = validateUserSignature(
              uid,timestamp,encodeToString(secret.getBytes(),false),signature);
    assert(yours == theirs);
}

Of course, as reproduced, the problem is with Commons Net, whereas Commons Codec appears to be fine.

Well I finally heard back from gigya yesterday regarding this issue, and it turns out their own server-side Java API exposes a method for handling this use case, SigUtils.validateUserSignature:

if (SigUtils.validateUserSignature(uid, timestamp, secretKey, signature)) { ... }

Today I was able to verify that this call is behaving correctly, so that solves the immediate issue and turns this whole post into a kind of a facepalm moment for me.

However:

I'm still interested in why my own home-rolled method doesn't work (and I have a bounty to award anyway). I'll examine it again this coming week and compare it with the SigUtils class file to try and figure out what went wrong.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!