代码之家  ›  专栏  ›  技术社区  ›  Community wiki

构造和验证Gigya签名

  •  10
  • Community wiki  · 技术社区  · 1 年前

    我根据gigya的 instructions for constructing a signature .这是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;
    }
    

    [sic]

    以下是我的方法(省略了异常处理):

    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's FAQ ,以防有帮助。

    为了进一步澄清这一点 以下为: uid , timestamp signature 都是从吉亚制作的饼干里拿走的。为了验证这些没有被欺骗,我采取了 流体 时间戳 ,并确保 签名 可以用我的密钥重建。事实上,它在不应该失败的时候失败了,这表明在这个过程的某个时刻,无论是我的方法、前端还是gigya本身,都存在错误/格式问题。这个问题的目的本质上是为了排除上述方法中的错误。

    注: 我还尝试过URL编码 流体 以下为:

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

    尽管我认为这无关紧要,因为它只是一个整数。同样的道理 时间戳

    更新:

    根本问题已经解决,但问题本身仍然悬而未决。看见 my answer 了解更多详细信息。

    更新2:

    事实证明,我对apache的哪一个感到困惑 基准64 类-我的代码没有使用 Commons Codec version 但是 Commons Net version 。这种困惑源于我的项目中有大量的第三方库,而我对许多库一无所知 基准64 Apache库多年来的实现——我现在意识到了这种情况 Commons Codec 是要解决的。在编码方面,我似乎迟到了。

    在Commons编解码器的版本中切换后,该方法的行为正确。

    我要把赏金奖励给 @erickson 自从 his answer 是对的,但请投票支持这两个答案,因为他们的洞察力很好!我会暂时保留赏金,让他们得到应有的关注。

    3 回复  |  直到 7 年前
        1
  •  10
  •   erickson    12 年前

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

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

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


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

    这个 encodeBase64String() 您正在使用的方法总是将CRLF附加到其输出中。Gigya签名不包括这个尾随空格。比较这些字符串的相等性失败,只是因为空格中存在这种差异。

    使用 encodeBase64String() 从Commons编解码器库(而不是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);
    }
    
        2
  •  7
  •   Community Dai    7 年前

    代码审核时间!我喜欢做这些。让我们检查一下您的解决方案,看看我们的处境如何。

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

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

    现在让我们逐步了解您的代码:

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

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

    ( 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");
    

    这很好( reference a , reference b )。 但是 将来,考虑使用 StringBuilder 对于显式连接字符串,而不是依赖 compiler-time optimizations to support this feature

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

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

    站住!这个坏了 encapsulation 。它在功能上是正确的,但如果您将其作为参数传递给方法,会比从另一个源中提取它更好(因此,在本例中,将您的代码与 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);
    

    是的,这是正确的( reference a , reference b , reference 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编码 定义的时间戳 here ?如果你没有做到这一点,你就是 会得到你期望的结果!

    2. 你确定密钥是正确的并且编码正确吗?请确保在调试器中检查此项!

    3. 就这一点而言,如果您可以访问Java或其他版本的签名生成算法,请在调试器中验证整个过程。如果做不到这一点,综合一个会帮助你检查你的工作,因为 the encoding caveats raised in the documentation

    还应该报告伪代码错误。

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


    编辑:

    我查过了 their implementation of Base64 反对 Apache Commons Codec's .测试代码:

    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 ,只是为了确定。

    这是 the source code for their signature generator ,sans Javadoc(作者署名:拉维夫·帕维尔):

    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似乎很好。

        3
  •  5
  •   Paul Bellora    12 年前

    昨天我终于收到了gigya关于这个问题的回复,结果发现他们自己的服务器端Java API公开了一种处理这个用例的方法, SigUtils.validateUserSignature 以下为:

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

    今天,我能够验证这个电话的行为是否正确,从而解决了眼前的问题,并将整个帖子变成了我的掌上明珠。

    但是:

    我仍然对为什么我自己的家庭滚动方法不起作用感兴趣(无论如何,我都有奖励)。我将在下周再次检查它,并将其与 SigUtils 类文件,试图找出问题所在。