Создание и проверка подписи Gigya

Я написал метод проверки подписи 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 даже когда это не должно быть. Может кто-нибудь заметить что-то не так с моей реализацией? Мне интересно, может ли быть проблема с вызывающим абонентом или самим гигом - "Ваш метод проверен" - верныйответ.

Я использую Apache Commons'Base64 класс для кодирования.

Дополнительная (несколько избыточная) информация о сигнатурах также содержится в FAQ Гиги, если это поможет.

Чтобы уточнить это далее: uid, timestamp, а также signature Все они взяты из файлов cookie, установленных Gigya. Чтобы убедиться, что они не были подделаны, я беру uid а также timestampи убедившись, signature может быть восстановлен с использованием моего секретного ключа. Тот факт, что он терпит неудачу, когда не должен указывать на ошибку / форматирование в какой-то момент процесса, либо с помощью моего метода, во фронтэнде, либо с самим gigya. Цель этого вопроса состоит в том, чтобы исключить ошибку в вышеуказанном методе.

Примечание: я также пробовал URL-кодирование uid:

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

Хотя я не думаю, что это будет иметь значение, потому что это просто целое число. То же самое касается timestamp,

Обновить:

Основная проблема была решена, однако сам вопрос остается открытым. Смотрите мой ответ для более подробной информации.

Обновление 2:

Оказывается, я не понял, какой из Apache Base64 классы, которые я использовал - в моем коде использовалась не версия Commons Codec, а версия Commons Net. Эта путаница возникла из-за большого количества сторонних библиотек моего проекта и моего незнания многих Base64 за многие годы реализации из библиотек Apache - теперь я понимаю, что Commons Codec должен был решить эту проблему. Похоже, я опаздываю на вечеринку, когда дело доходит до кодирования.

После переключения в версии Commons Codec, метод работает правильно.

Я собираюсь присудить вознаграждение erickson, так как его ответ был точен, но, пожалуйста, подтвердите оба ответа за их отличную проницательность! Я пока оставлю награду открытой, чтобы они привлекли внимание, которого они заслуживают.

3 ответа

Решение

Ну, я наконец-то получил ответ от gigya вчера об этой проблеме, и оказалось, что их собственный серверный API Java на стороне сервера предоставляет метод для обработки этого варианта использования, SigUtils.validateUserSignature:

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

Сегодня я смог убедиться, что этот вызов ведет себя правильно, так что это решает насущную проблему и превращает весь этот пост в своего рода лицевой момент для меня.

Тем не мение:

Меня все еще интересует, почему мой собственный домашний метод не работает (и в любом случае у меня есть награда). Я рассмотрю это снова на следующей неделе и сравню с SigUtils файл класса, чтобы попытаться выяснить, что пошло не так.

Я бы внимательно посмотрел на вашу кодировку и декодирование 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);
}

Время пересмотра кода! Я люблю делать это. Давайте проверим ваше решение и посмотрим, где мы упадем.

В прозе наша цель состоит в том, чтобы подчеркнуть, соединить временную метку и 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");

Это нормально ( ссылка а, ссылка б). Но в будущем рассмотрите возможность использования StringBuilder для конкатенации строк в явном виде, вместо того чтобы полагаться на оптимизацию во время компиляции для поддержки этой функции.

Обратите внимание, что документация до этого момента не согласуется с тем, использовать ли "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. Вы кодируете UTF-8 свою входную строку для своего UID или вашей временной метки, как определено здесь? Если вы не смогли этого сделать, вы не получите ожидаемых результатов!

  2. Вы уверены, что секретный ключ правильный и правильно закодирован? Обязательно проверьте это в отладчике!

  3. В этом отношении, проверьте все это в отладчике, если у вас есть доступ к алгоритму генерации подписи, в Java или иным образом. Если это не удастся, синтез одного из них поможет вам проверить свою работу из-за предостережений по кодированию, о которых говорится в документации.

Также следует сообщить об ошибке псевдокода.

Я считаю, что проверка вашей работы здесь, особенно ваших строковых кодировок, покажет правильное решение.


Редактировать:

Я проверил их реализацию Base64 против кодека Apache Commons. Тестовый код:

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 (автор кредита: Равив Павел):

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, кажется, в порядке.

Другие вопросы по тегам