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