Проверка подписи в Java

Здравствуйте.

Имеется токен и RSA-ключи на нем. Нужно подписать данные при помощи токена и потом проверить подпись на удаленной машине, где токена нет. Мой порядок действий:

1) Извлекается ключ при помощи session.findObjects()
2) Данные для подписи хешируются и подписываются через object.sign(data, Mechanism.SIGN_RSA)
3) Через findObjects() извлекается публичный ключ, из него при помощи getAttributeValue() извлекаются значения аттрибутов CKA_PUBLIC_EXPONENT и CKA_MODULUS
4) Из модуля и публичной экспоненты собирается публичный ключ через KeyFactory.generatePublic()

Дальше возникает проблема. Как я понял, в функцию sign передаются уже хешированные данные, т.к. никаких намеков на выбор алгоритма хеширования там нету. Однако, как проверить подпись, я не знаю. Попытка сделать это через класс Signature проваливается с возвратом false на методе verify(), попытка "заглянуть внутрь" при помощи класса Cipher, расшифровав подпись при помощи публичного ключа, проваливается с исключением "Message is larger than modulus".

Код, делающий описанные выше вещи: https://pastebin.com/sdCnhADC

(2014-08-02 22:00:05 отредактировано makkarpov)

Re: Проверка подписи в Java

Глянув на отрицательный modulus после конвертирования в BigInteger, понял, что он должен быть конвертирован без учета знака. Исправленный вариант для тех, кто будет ковыряться:

1) Создавать числа не через new BigInteger(byte[]), а через new BigInteger(1, byte[])
2) Подпись представляет из себя аргумент функции sign, зашифрованный в режиме RSA/ECB/PKCS1Padding с ключом на токене. Соответственно, обратное преобразование - расшифровать подпись в таком режиме и проверить на совпадение с тем, что передавалось в функцию sign().

Код:

Session tokenSession = token.createSession();
tokenSession.login(pin);
            
TokenObj privKey = tokenSession.findObjects(new AttributeList(new Attribute[]{
    Attribute.CLASS_PRIVATE_KEY, Attribute.PRIVATE_TRUE, Attribute.TOKEN_TRUE,
    new Attribute(PKCS11Constants.CKA_LABEL, ruToken.KEY_LABEL)
}))[0];
            
byte[] signature = privKey.sign(CryptoUtils.sha256(contents), Mechanism.SIGN_RSA);
    
BigInteger modulus = new BigInteger(1, (byte[]) privKey.getAttributeValue(PKCS11Constants.CKA_MODULUS));
BigInteger pubExp  = new BigInteger(1, (byte[]) privKey.getAttributeValue(PKCS11Constants.CKA_PUBLIC_EXPONENT));
        
PublicKey localKey = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, pubExp));
            
Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding");
c.init(Cipher.DECRYPT_MODE, localKey);
System.out.println(Arrays.equals(c.doFinal(signature), CryptoUtils.sha256(contents)));

Re: Проверка подписи в Java

Судя по коду, вы используете микс из внутренней реализации JRT11 провайдера (к слову, не предназначенной для использования извне) и интерфейса JCA c sunPKCS11 провайдером. Да, у вас получилось найти ключи на токене, извлечь их и корректно сконвертировать для JCA, получить хеш сторонней утилитой и проверить подпись через интерфейс расшифрования. Вероятно, в итоге вы получили верный результат, а вместе с тем тяжелый код и использование нескольких технологий одновременно, в том числе и не предназначенных для вашей целей.

Можно было бы использовать JRT провайдер через предназначенный для его использования JCA интерфейс. Тогда бы код сократился до:

Config config = new Config("C:\\Windows\\System32\\rtPKCS11ECP.dll", "", "12345678");
int pos = Security.addProvider(new ru.rutoken.jrt11.JRT11Provider(config));

KeyStore keyStore = KeyStore.getInstance("rtStore", "JRT11");
keyStore.load(null, "12345678".toCharArray());

PrivateKey privateKey = (PrivateKey)keyStore.getKey("Sample RSA Private Key", null);
KeyStore.Entry entry = keyStore.getEntry("Sample RSA Public Key", null);
PublicKey publicKey = ((ru.rutoken.security.KeyContainer)entry).getPublicKey();
byte[] data = "Message".getBytes();

Signature signature = Signature.getInstance(JRT11Provider.MD5_RSA_SIGNATURE_ALGORITHM, "JRT11");
signature.initSign(privateKey);
signature.update(data);
byte[] signatureValue = signature.sign();

signature.initVerify(publicKey);
signature.update(data);
boolean result = signature.verify(signatureValue);

На сегодня JRT11 провайдер умеет работать с подписью с RSA только со связкой MD5. Полное руководство по провайдеру можно найти тут: http://dev.rutoken.ru/pages/viewpage.ac … Id=4227190

Можно было бы использовать JCA интерфейс через sunPKCS11 провайдер полностью. Единственное -- для работы с ключами на токене необходимо создать и записать на токен сертификат для ключевой пары.

Можно было бы использовать функции PKCS11 библиотеки rtPKCS11.dll/rtPKCS11ECP.dll напрямую через JNI и sun.security.pkcs11.wrapper, через наш враппер или IAIK PKCS#11 Wrapper. Тут код полностью аналогичен сишному (присутствует в Рутокен SDK), абсолютно корректен и не имеет никаких ограничений в реализациях.

Re: Проверка подписи в Java

JCA через sunPKCS11 я не использовал. Проверка подписи через Cipher - оффлайн операция по отношению к токену, и она использует стандартный провайдер JVM. Собственно, ради это все и затевалось - необходимо проверить подпись, сделанную токеном на другой машине, которая токена не имеет. И для этого надо экспортировать публичный ключ в стандартном формате, и проверять при помощи других инструментов. Ибо, как я видел по коду, ваш класс Signature делает ровно то же самое - создает класс MessageDigest, извлекает TokenObj из ключа, который ему дали и в процессе подписи вызывает TokenObj.sign(). Что, кстати, не совместимо с форматом подписи в Signature, ибо там подписывается ASN.1 объект. В этом же приложении надо было расшифровывать RSA через токен. Судя по исходному коду класса Cipher, он умеет только ГОСТ-овые алгоритмы, а через TokenSession.decryptSingle - получилось.

Кроме того, мне не нравится сам принцип построения JCA API, ибо по моим представлением, работа с токеном идет по алгоритму:

1) Нашли все токены в системе
2) По какому-то признаку (серийник, метка, или токен всего один подключен) выбрали нужный
3) Залогинились на токен с PIN-кодом
4) Начали выполнять криптографические операции

Единственное, что более-менее удовлетворяет таким критериям - класс SessionFactory, который описан у вас в документации. Собственно, от него я и начинал изучение внутреннего API и обнаружил, что SessionFactory корнями уходит в JRT11. Остальные же классы - Cipher, Signature, KeyPairGenerator нуждаются, как минимум, в указании PIN-кода каждый раз, а по-хорошему - в задании серийника, чтобы не получилось так, что ты сгенерировал ключ на одном токене, а попытался подписать на другом. И, как я уже сказал, имеются проблемы совместимости. Подпись, сгенерированная вашим классом Signature, только на нем и проверяется, методы SHA256withRSA и SHA256withrtRSA несовместимы. Ключи возвращают null для getEncoded(), и прочие существенные недостатки.

Re: Проверка подписи в Java

Вы правы, в предложенном коде не была учтена необходимость экспорта публичного ключа. Провайдер JRT11 разрабатывался в первую очередь для работы с ГОСТами и сейчас не имеет нужной вам функциональности.
У разных провайдеров будут отличаться форматы и механизмы, от этого никак не уйдешь. Пытаться эмпирическим путем нащупать лазейку мимо интерфейсов сквозь внутренние API, чтобы привести все к единому знаменателю -- не самый простой, красивый и безопасный путь.

Кроме того, мне не нравится сам принцип построения JCA API, ибо по моим представлением, работа с токеном идет по алгоритму:
1) Нашли все токены в системе
2) По какому-то признаку (серийник, метка, или токен всего один подключен) выбрали нужный
3) Залогинились на токен с PIN-кодом
4) Начали выполнять криптографические операции

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

Работа напрямую с PKCS11 библиотекой через связку JNI + sun.wrapper подошла бы лучше. Собственно, вы уже так работаете, только через внутренности нашего провайдера. Из необходимых требований -- наличие нашей библиотеки (устанавливается вместе с драйверами).

Так или иначе -- благодарим вас за фидбек, посыл пойман, будем стараться делать наш продукт лучше.