10

根据 Gigya 的构造签名的说明,我编写了一个方法来根据指定的时间戳和 UID 验证 gigya 签名。这是 Gigya 的代码:

string constructSignature(string timestamp, string UID, string secretKey) {
    // Construct a "base string" for signing
    baseString = timestamp + "_" + UID;
    // Convert the base string into a binary array
    binaryBaseString = ConvertUTF8ToBytes(baseString);
    // Convert secretKey from BASE64 to a binary array
    binaryKey = ConvertFromBase64ToBytes(secretKey);
    // Use the HMAC-SHA1 algorithm to calculate the signature 
    binarySignature = hmacsha1(binaryKey, baseString);
    // Convert the signature to a BASE64
    signature = ConvertToBase64(binarySignature);
    return signature;
}

[原文如此]

这是我的方法(省略异常处理):

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

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

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

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

    // 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);

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

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

false即使不应该,此方法也会返回。任何人都可以发现我的实施有问题吗?我想知道调用者或 gigya 本身是否存在问题 - “您的方法已检出”是一个有效的答案

我正在使用 Apache Commons 的Base64类进行编码。

在Gigya 的常见问题解答中还可以找到有关签名的更多(有些多余的)信息,以防万一。

为了进一步澄清这一点uid、、timestampsignature都取自 gigya 设置的 cookie。为了验证这些没有被欺骗,我正在使用uidand timestamp,并确保signature可以使用我的密钥进行重建。当它不应该在过程中的某个时刻指示错误/格式问题时,它失败的事实,无论是使用我的方法,在前端,还是使用 gigya 本身。这个问题的目的本质上是排除上述方法中的错误。

注意:我也尝试过 URL-encoding uid

String baseString = timestamp + "_" + URLEncoder.encode(uid, "UTF-8");

虽然我认为这并不重要,因为它只是一个整数。也是如此timestamp

更新:

根本问题已经解决,但问题本身仍然悬而未决。有关更多详细信息,请参阅我的答案

更新 2:

事实证明,我对我使用的是哪个 apacheBase64类感到困惑——我的代码使用的不是Commons Codec 版本,而是Commons Net 版本。这种困惑源于我的项目中有大量第三方库,以及我Base64多年来对 Apache 库的许多实现一无所知——我现在意识到Commons Codec旨在解决这种情况。看来我在编码方面迟到了。

在切换 Commons Codec 的版本后,该方法的行为正确。

我将把赏金奖励给@erickson,因为他的回答很到位,但请为他们出色的洞察力投票支持这两个答案!我将暂时开放赏金,以便他们得到应有的关注。

4

3 回答 3

10

我会仔细看看你的 Base-64 编码和解码。

您是否为此使用第三方库?如果有,是哪一个?如果没有,您能否发布您自己的实现或至少一些示例输入和输出(用十六进制表示字节)?

有时使用的“额外”Base-64 字符会有所不同(用字符替换“/”和“+”)。也可以省略填充,这会导致字符串比较失败。


正如我所怀疑的,正是 Base-64 编码导致了这种差异。但是,导致问题的是尾随空格,而不是填充或符号的差异。

您使用的encodeBase64String()方法始终将 CRLF 附加到其输出中。Gigya 签名不包括此尾随空格。比较这些字符串是否相等仅因为空格的差异而失败。

使用encodeBase64String()Commons Codec 库(而不是 Commons Net)来创建有效的签名。

如果我们将签名计算分解,并针对 Gigya SDK 的验证器测试其结果,我们可以看到删除 CRLF 会创建一个有效的签名:

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);
}
于 2012-04-13T05:23:25.893 回答
7

代码审查时间!我喜欢做这些。让我们检查您的解决方案,看看我们在哪里失败。

在散文中,我们的目标是将时间戳和 UID 下划线连接在一起,将 UTF-8 的结果强制转换为字节数组,将给定的 Base64 密钥强制转换为第二个字节数组,SHA-1将两个字节数组连接在一起,然后将结果转换回Base64。很简单,对吧?

(是的,那个伪代码有一个错误。)

现在让我们逐步检查您的代码:

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

您在这里的方法签名很好。虽然很明显,您需要确保您创建的时间戳和您正在验证的时间戳使用完全相同的格式(否则,这将始终失败)并且您的字符串是 UTF-8 编码的。

有关字符串编码如何在 Java 中工作的更多详细信息

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

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

这很好(参考 a参考 b)。但是,在未来,考虑StringBuilder显式地使用 for 字符串连接,而不是依赖编译器时的优化来支持这个特性

请注意,到目前为止,文档在使用“UTF-8”还是“UTF8”作为您的字符集标识符方面是不一致的。不过,“UTF-8”是公认的标识符;我相信“UTF8”是为了遗留和兼容性目的而保留的。

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

拿着它!这打破了封装。它在功能上是正确的,但是如果您将它作为参数传递给您的方法,而不是从另一个源中拉入它会更好(在这种情况下,将您的代码耦合到 的细节MyConfig)。否则,这也很好。

    // 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);

是的,这是正确的(参考 a参考 b参考 c)。我这里没有什么要补充的。

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

正确,并且...

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

... 正确的。忽略警告和实现说明,您的代码会按程序检查。

不过,我会推测几点:

  1. 您是否按照此处定义的方式对您的 UID时间戳的输入字符串进行了 UTF-8 编码?如果你没有这样做,你将不会得到你期望的结果!

  2. 您确定密钥正确且编码正确吗?确保在调试器中检查这一点!

  3. 就此而言,如果您可以使用 Java 或其他方式访问签名生成算法,请在调试器中验证整个事情。如果做不到这一点,合成一个将帮助您检查您的工作,因为文档中提出了编码警告

也应该报告伪代码错误。

我相信在这里检查您的工作,尤其是您的字符串编码,将揭示正确的解决方案。


编辑:

我检查了他们Base64Apache Commons Codec 的实现。测试代码:

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);
    }
}

简单的测试表明它们的输出具有可比性。输出:

dGhpcyBpcyBteSB0ZXN0IHN0cmluZw==
dGhpcyBpcyBteSB0ZXN0IHN0cmluZw==

我在调试器中验证了这一点,以防万一我在视觉分析中无法检测到空格并且断言没有命中。它们是相同的。为了确定,我还检查了一段lorem ipsum 。

这是他们的签名生成器的源代码,没有 Javadoc(作者信用: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);           
}

根据我在上面所做的一些更改更改您的函数签名并运行此测试用例会导致两个签名都得到正确验证:

// 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);
}

当然,正如转载的那样,问题出在 Commons Net 上,而 Commons Codec 似乎没问题。

于 2012-04-13T02:25:50.990 回答
5

好吧,我昨天终于从 gigya 那里收到了关于这个问题的回复,事实证明他们自己的服务器端 Java API 公开了一种处理这个用例的方法,SigUtils.validateUserSignature

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

今天,我能够验证此呼叫的行为是否正确,从而解决了当前的问题,并将整个帖子变成了对我来说的一种面对面的时刻。

然而:

我仍然对为什么我自己的自制方法不起作用感兴趣(而且我有赏金要奖励)。下周我将再次检查它,并将其与SigUtils类文件进行比较,以尝试找出问题所在。

于 2012-04-14T20:58:06.867 回答