:: Visual Foxpro, Foxpro for DOS
Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Удалось ли кому-нибудь средствами VFP при помощи Crypto API Крипто-Про подписать ЭЦП сформированный XML-файл?

Алгоритм таков:
1. Убеждаемся, что сертификат (открытый ключ ЭЦП Крипто-Про) установлен из файла .cer в персональное хранилище (personal). Если нет, то делаем DblClick на файле .cer и устанавливаем его.
2. Получаем контекст пользовательского сертификата (открытого ключа), не обращая внимание на наличие корневых сертификатов на компьютере подписанта:
hStore = CertOpenStore(CERT_STORE_PROV_FILENAME,..)
CertFindCertificateInStore(hStore,...)
3. Затем, создаем на основе него подпись:
CryptSignMessage(...)

На сайте у Анатолия Могилевца, находим описание типов и немного полезной информации:
#DEFINE PKCS_7_ASN_ENCODING 0x00010000
#DEFINE X509_ASN_ENCODING 1
#DEFINE CERT_FIND_ANY 0
DECLARE INTEGER CryptCreateHash IN advapi32;
INTEGER hProv,;
INTEGER Algid,;
INTEGER hKey,;
INTEGER dwFlags,;
INTEGER @ phHash
DECLARE INTEGER CryptHashData IN advapi32;
INTEGER hHash,;
STRING @ pbData,;
LONG dwDataLen,;
LONG dwFlags
DECLARE INTEGER CryptGetHashParam IN advapi32;
INTEGER hHash,;
LONG dwParam,;
STRING @ pbData,;
LONG @ pdwDataLen,;
LONG dwFlags
DECLARE INTEGER CryptDestroyHash IN advapi32;
INTEGER hHash
DECLARE INTEGER CertFindCertificateInStore IN crypt32;
INTEGER hCertStore,;
LONG dwCertEncodingType,;
LONG dwFindFlags,;
LONG dwFindType,;
INTEGER pvFindPara,;
INTEGER pPrevCertContext
DECLARE INTEGER CertCloseStore IN crypt32;
INTEGER hCertStore,;
LONG dwFlag
LOCAL hBuffer
hBuffer = 0
hBuffer = CertFindCertificateInStore(hStore,;
BITOR(PKCS_7_ASN_ENCODING, X509_ASN_ENCODING),;
0, CERT_FIND_ANY, 0, 0)
? "Pointer to CERT_CONTEXT structure:", hBuffer
IF hBuffer <> 0
= CertFreeCertificateContext(hBuffer)
ENDIF
Деклараций API-функций CertOpenStore и CertOIDToAlgId там, увы, нет!

P.S.: В документе "Методические рекомендации по разработке электронных сервисов и применению технологии электронной подписи при межведомственном электронном взаимодействии. Версия 2.4.4 (исправлены технические ошибки версии 2.4.3)", доступном для загрузке по ссылке: smev.gosuslugi.ru, на стр.41 указано: "К элементу <soapenv:Body> и его потомкам, включая атрибуты, применяется каноникализация www.w3.org#, на основе результата рассчитывается хэш по алгоритму ГОСТ Р 34.11-94 и заносится в <dsigestValue> в формате Base64."

Для реализации алгоритма ГОСТ Р 34.11-94 необходимо и достаточно переписать на VFP функцию:
function GetDigestValue(sText: string; var hProv: HCRYPTPROV; var Cert: PCCERT_CONTEXT): string;
var s, err: string;
i: integer;
size, hashSize, DataSize: DWORD;
hash: HCRYPTHASH;
pbHash: PByte;
alg: ALG_ID;
begin
Result:='';
// alg:=CALG_GR3411;
// CALG_GR3411 - это идентификатор алгоритма хэширования по ГОСТ Р 34.11-94 криптопровайдера КриптоПро CSP
// Поскольку числовое значение CALG_GR3411 не определено в wincrypt.h из FFC-классов VFP,
// определим его для VFP на основе файла WinCryptEx.h, входящего в состав КриптоПро SDK,
// доступного для загрузки по адресу: hттp://www.cryptopro.ru/products/csp/downloads :
// #DEFINE dnALG_SID_GR3411 30
// #DEFINE dnCALG_GR3411 BITOR(BITOR(dnALG_CLASS_HASH,dnALG_TYPE_ANY),dnALG_SID_GR3411)
alg:=CertOIDToAlgId(Cert.pCertInfo.SignatureAlgorithm.pszObjId);
// create a hash object that uses CALG_GR3411 algorithm
if not CryptCreateHash(hProv, alg, 0, 0, hash) then
begin
raise Exception.Create('Ошибка создания хеш-объекта');
end;
// add the input data to the hash object
s:=sText;
size:=Length(s);
if not CryptHashData(hash, PByte(s), size, 0) then
begin
raise Exception.Create('Ошибка при хешировании');
end;
DataSize:=SizeOf(DataSize);
if not CryptGetHashParam(hash, HP_HASHSIZE, @hashSize, DataSize, 0) then
begin
raise Exception.Create('Ошибка при хешировании');
end;
GetMem(pbHash, hashSize);
// retrieve the hash value, if caller did not provide enough storage (16 bytes for MD5)
// this will fail with ERROR_MORE_DATA and hashSize will contain needed storage size
try
if not CryptGetHashParam(hash, HP_HASHVAL, pbHash, hashSize, 0) then // получаем хеш
begin
raise Exception.Create('Ошибка при хешировании');
end;
for i:=0 to hashSize-1 do // выводим хеш
Result:=Result+PAnsiChar(pbHash)(i);
finally
FreeMem(pbHash);
end; //finally
// free the hash object
if not CryptDestroyHash(hash) then
begin
raise Exception.Create('Ошибка при уничтожении хеша', False);
end;
end;
Для этого потребуется написать корректные декларации функций CertOpenStore и CertOIDToAlgId.

Также, стоит задача, как преобразовать pbyte в строку UTF-8 и закодировать ее в формате Base64, т.е. нужно написать прямую и обратную процедуры для перекодировки:
? decode_base64(encode_base64(encode("UTF-8", "Тестовая строка")))
Как известно, в VFP есть функция перекодировки в UTF-8:
StrToFile(STRCONV(FileToStr("C:\file1251.txt"), 9), "C:\fileUTF8.txt")
Возможно ли средствами VFP закодировать файл в UTF-8 в Base64?



Исправлено 5 раз(а). Последнее : rvc44, 01.07.12 08:19
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Igor Korolyov

Сообщений: 34580
Дата регистрации: 28.05.2002
rvc44
Как известно, в VFP есть функция перекодировки в UTF-8
...
Возможно ли средствами VFP закодировать файл в UTF-8 в Base64?
Там же есть и кодирование/декодирование base64 - 2 параметр =13 и 14 в функции STRCONV - но только не ФАЙЛА а строки - а уж поместится ли твой файл в фоксовую строку, или вылезет за лимит - это только тебе известно...


------------------
WBR, Igor
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Спасибо, Игорь! Раньше в VFP максимальная длина строки была равна 255 символов, а теперь стала 64 Kb! Так что, нужно просто следить за тем, чтобы XML-файл не превышал этот размер... вот и всё!

Насчет двух недостающих деклараций API-функций, как выяснилось на практике получается вот так:
DECLARE INTEGER CertOIDToAlgId IN crypt32;
STRING pszObjId
DECLARE INTEGER CertOpenStore IN crypt32;
INTEGER lpszStoreProvider,;
LONG dwEncodingType,;
INTEGER hCryptProv,;
LONG dwFlags,;
STRING pvPara
Теперь можно функцию GetDigestValue на VFP переписать!



Исправлено 4 раз(а). Последнее : rvc44, 05.07.12 00:36
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Igor Korolyov

Сообщений: 34580
Дата регистрации: 28.05.2002
Максимальная длина строковой переменной (что и имеет значение в данном случае) в VFP чуть менее 16Мб - по крайней мере начиная с VFP5. В принципе через FILETOSTR можно получить и более длинную строку, но вот работать с ней скорее всего не выйдет (большинство функций откажутся с ней работать). Учитывая что base64 увеличивает размер обрабатываемого объекта (массива байт/строки - это не суть важно) на треть (каждые 3 байта кодируются 4-мя символами) и что UTF8 преобразует русские буковки в 2-х байтные последовательности (английские буковки, цифры и большая часть спецсимволов - пробелы там, кавычки с точками и т.п. остаётся 1-байтными), то взяв наихудший случай можно посчитать, что для подобного двойного преобразования исходный "текст" (совсем неважно XML там или нет) должен быть не более 6Мб. Если он больше, то нужно уже использовать иные средства преобразования. Ещё можно использовать вместо строковой переменной memo-поле, для него пределом будет чуть менее 2Гб - но это уже из разряда извращений, да и не уверен я что STRCONV сможет его нормально пережевать.


------------------
WBR, Igor
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Надо же, действительно, больше 64 Kb! А я раньше считал это лимитом. Известный разработчик VFP Rick Strahl написал статью 19.01.2012 "How to work around Visual FoxPro's 16 Megabyte String Limit": www.west-wind.com. Можно почитать, кому интересно!
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Есть ли у кого возможность проверить мой следующий код для поиска установленных сертификатов в системе, в котором изпользуется стандартное Crypto API:
#DEFINE CERT_STORE_PROV_SYSTEM 10
#DEFINE CERT_FIND_ANY 0
#DEFINE CERT_FIND_SUBJECT_STR 0x00080007
#DEFINE PKCS_7_ASN_ENCODING 0x00010000
#DEFINE X509_ASN_ENCODING 0x00000001
#DEFINE CERT_SYSTEM_STORE_CURRENT_USER 0x00010000
DECLARE INTEGER CertOpenStore IN crypt32;
INTEGER lpszStoreProvider,;
LONG dwEncodingType,;
INTEGER hCryptProv,;
LONG dwFlags,;
STRING pvPara
*-- 5-й параметр STRING, а не INTEGER, если 4-й параметр CERT_FIND_SUBJECT_STR, а не CERT_FIND_ANY, как в данном примере
DECLARE INTEGER CertFindCertificateInStore IN crypt32;
INTEGER hCertStore,;
LONG dwCertEncodingType,;
LONG dwFindFlags,;
LONG dwFindType,;
INTEGER pvFindPara,;
INTEGER pPrevCertContext
DECLARE INTEGER CertFreeCertificateContext IN crypt32;
INTEGER pCertContext
DECLARE INTEGER CertCloseStore IN crypt32;
INTEGER hCertStore,;
LONG dwFlag
DECLARE INTEGER GetLastError ;
IN WIN32API AS GetLastError
LOCAL lnStoreProvider, lcPara, hStoreHandle, pCertContext, lnResultCode
*-- Открываем хранилище сертификатов
lnStoreProvider = CERT_STORE_PROV_SYSTEM && 10
*-- Наименование персонального хранилища "Личные" ("My") текущего пользователя (а не локального компьютера)
lcPara = STRCONV("MY" + CHR(0), 5)
hStoreHandle = CertOpenStore(lnStoreProvider, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, lcPara)
If !hStoreHandle = 0
*-- Получаем указатель на наш сертификат
pCertContext = 0
*-- Найдем все сертификаты
lnFindPara = 0
lnPrevCertContext = 0
pCertContext = CertFindCertificateInStore(hStoreHandle,;
BITOR(PKCS_7_ASN_ENCODING, X509_ASN_ENCODING),;
0, CERT_FIND_ANY, lnFindPara, lnPrevCertContext)
If !pCertContext = 0
=MessageBox("Хранилище сертификатов MY открылось успешно!"+Chr(13)+;
"Сертификаты найдены!",64,"Сообщение")
= CertFreeCertificateContext(pCertContext)
Else
lnResultCode = GetLastError()
=MessageBox("Хранилище сертификатов MY открылось, однако,"+Chr(13)+;
"обнаружить сертификаты не удалось!",16,"Ошибка: " + Transform(lnResultCode))
EndIf
Else
lnResultCode = GetLastError()
=MessageBox("Невозможно открыть хранилище MY.",16,"Ошибка: " + Transform(lnResultCode))
EndIf
= CertCloseStore(hStoreHandle, 0)
У меня хранилище сертификатов открывается без проблем, а вот функция CertFindCertificateInStore почему-то стабильно возвращает 0, хотя один сертификат в системе установлен... Требуется помощь зала! ))



Исправлено 1 раз(а). Последнее : rvc44, 06.07.12 19:04
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
dimuhametov

Сообщений: 1562
Откуда: Костанай
Дата регистрации: 01.11.2008
Аналогично CertFindCertificateInStore возвращает 0 , хотя 2 сертификата GOST и RSA установлены в системе.


------------------
Незнание делает жизнь такой интересной.
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Igor Korolyov

Сообщений: 34580
Дата регистрации: 28.05.2002
Мануал говорит про использование UNICODE строк. У меня фокс вываливается в C005 вскоре после подобного издевательского обмана системы - нужно то было писать
lcPara = STRCONV("MY" + CHR(0), 5)
Дополнительный CHR(0) обязателен, т.к. фокс при передаче "строк" в АПИ функции добавляет только 1 chr(0) терминатор, а для unicode нужен "двойной нуль" в конце строки.
P.S. После подобного исправления у меня лично находит имеющийся в хранилище "личный" сертификат.


------------------
WBR, Igor
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Браво!
+1, Игорь, как всегда! Низко кланяюсь... Это как раз то, что я сегодня целый день искал.

Теперь, если переопределить 5-й параметр через STRING в
DECLARE INTEGER CertFindCertificateInStore IN crypt32;
INTEGER hCertStore,;
LONG dwCertEncodingType,;
LONG dwFindFlags,;
LONG dwFindType,;
STRING pvFindPara,;
INTEGER pPrevCertContext
и поменять вызов Crypto API функции CertFindCertificateInStore на
lcFindPara = STRCONV("CN моего сертификата" + CHR(0), 5)
lnPrevCertContext = 0
pCertContext = CertFindCertificateInStore(hStoreHandle,;
BITOR(PKCS_7_ASN_ENCODING, X509_ASN_ENCODING),;
0, CERT_FIND_SUBJECT_STR, lcFindPara, lnPrevCertContext)
с использованием в качестве 4-го параметра CERT_FIND_SUBJECT_STR, то VFP и указанный сертификат находит без проблем!
CN сертификата можно посмотреть в Windows оснастке certmgr.msc для тех, у кого не установлен CryptoPro (последняя версия 3.6), либо в оснастке "Сертификаты" от CryptoPro. CryptoPro необходим для подписывания передаваемых XML-файлов по алгоритму ГОСТ Р 34.11-94 (CALG_GR3411). Разработка темы продолжается...

Конечно, есть еще непонятные вопросы, например, как в фоксе выделять память при поиске сертификата по hash (#DEFINE CERT_FIND_HASH 0x00010000), рассмотренной меньше недели назад на форуме Крипто-Про: ats.cryptopro.ru
... но, по крайней мере, радует то, что это работает не только в C++, C#, Delphi, но и под VFP (если очень захотеть, и когда на FoxClub'е есть такие люди как Игорь!)
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Влад Колосов

Сообщений: 22664
Откуда: Ростов-на-Дону
Дата регистрации: 05.05.2005
У меня по-прежнему не находит.


------------------
Совершенство - это не тогда, когда нельзя
ничего прибавить, а тогда, когда нечего убавить.
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Igor Korolyov

Сообщений: 34580
Дата регистрации: 28.05.2002
rvc44
как в фоксе выделять память при поиске сертификата по hash (#DEFINE CERT_FIND_HASH 0x00010000)
Плоская структура без наворотов - делается тривиально - никакого специального выделения памяти в этом случае не требуется.
lcHash = 0h123456789abcdef01234567890abcdef... && сам поток байтиков.
* Если поток байтиков закодирован в строчке вида "123456789abcdef"
* то можно использовать STRCONV( ,16)
lcBuf = BINTOC(LEN(m.lcHash), "4RS") + m.lcHash
* этот lcBuf и передавать


------------------
WBR, Igor
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Влад, а твой сертификат точно в разделе "текущий пользователь", как показано на скриншоте?

[attachment 13759 Cert.JPG]

После успешной отработки функции CertFindCertificateInStore, т.е. если сертификат найден, можно получить и вывести имя субъекта сертификата:
#DEFINE MAX_NAME 256
DECLARE INTEGER CertGetNameStringA IN crypt32;
INTEGER pCertContext,;
LONG dwType,;
LONG dwFlags,;
LONG pvTypePara,;
STRING @pszNameString,;
INTEGER cchNameString
*--Сертификат был найден. Получим и выведем имя субъекта сертификата.
szNameString = Replicate(Chr(0), MAX_NAME)
cchString = MAX_NAME
nChars = CertGetNameStringA(pCertContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, 0, @m.szNameString, cchString)
If nChars > 1
=MessageBox("Хранилище сертификатов MY открылось успешно!"+Chr(13)+;
"Сертификаты найдены!"+Chr(13)+;
"The message signer is:"+Chr(13)+;
szNameString,64,"Сообщение")
Else
lnResultCode = GetLastError()
=MessageBox("Getting the signer name failed",16,"Ошибка: " + Transform(lnResultCode))
EndIf



Исправлено 1 раз(а). Последнее : rvc44, 05.07.12 18:31
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
dimuhametov

Сообщений: 1562
Откуда: Костанай
Дата регистрации: 01.11.2008
Аналогично, как у Влада, CertFindCertificateInStore возвращает 0
[attachment 13763 1.jpg]


------------------
Незнание делает жизнь такой интересной.
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Тогда проверьте, работает ли у вас следующий код для поиска и вывода названий ВСЕХ сертификатов, установленных в персональном хранилище MY:

#DEFINE CERT_STORE_PROV_SYSTEM 10
#DEFINE CERT_SYSTEM_STORE_CURRENT_USER 0x00010000
#DEFINE CERT_NAME_EMAIL_TYPE 1
#DEFINE CERT_NAME_RDN_TYPE 2
#DEFINE CERT_NAME_ATTR_TYPE 3
#DEFINE CERT_NAME_SIMPLE_DISPLAY_TYPE 4
#DEFINE CERT_NAME_FRIENDLY_DISPLAY_TYPE 5
#DEFINE CERT_NAME_DNS_TYPE 6
#DEFINE CERT_NAME_URL_TYPE 7
#DEFINE CERT_NAME_UPN_TYPE 8
#DEFINE CERT_NAME_ISSUER_FLAG 0x1
#DEFINE CERT_NAME_DISABLE_IE4_UTF8_FLAG 0x00010000
#DEFINE MAX_NAME 256
DECLARE INTEGER CertOpenStore IN crypt32;
INTEGER lpszStoreProvider,;
LONG dwEncodingType,;
INTEGER hCryptProv,;
LONG dwFlags,;
STRING pvPara
DECLARE INTEGER CertEnumCertificatesInStore IN crypt32;
INTEGER hCertStore,;
INTEGER pPrevCertContext
DECLARE INTEGER CertGetNameStringA IN crypt32;
INTEGER pCertContext,;
LONG dwType,;
LONG dwFlags,;
LONG pvTypePara,;
STRING @pszNameString,;
INTEGER cchNameString
DECLARE INTEGER CertFreeCertificateContext IN crypt32;
INTEGER pCertContext
DECLARE INTEGER CertCloseStore IN crypt32;
INTEGER hCertStore,;
LONG dwFlag
DECLARE INTEGER GetLastError ;
IN WIN32API AS GetLastError
*-- Открываем хранилище сертификатов
lnStoreProvider = CERT_STORE_PROV_SYSTEM
*-- Наименование персонального хранилища "Личные" ("MY") текущего пользователя (а не локального компьютера)
lcPara = STRCONV("MY" + CHR(0), 5)
hStoreHandle = CertOpenStore(lnStoreProvider, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, lcPara)
If !hStoreHandle = 0
*-- Получение списка всех доступных сертификатов в персональном хранилище "Личные" ("MY")
pCertContext = 0
pCertContext = CertEnumCertificatesInStore(hStoreHandle, pCertContext)
Do While !pCertContext = 0
*--Сертификат был найден. Получим и выведем имя субъекта сертификата.
szNameString = Replicate(Chr(0), MAX_NAME)
cchString = MAX_NAME && 256
*-- Данный вариант находит наименования самих сертификатов
nChars = CertGetNameStringA(pCertContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, 0, @m.szNameString, cchString)
* *-- С флагом CERT_NAME_ISSUER_FLAG находятся не сертификаты, а УСЦ:
* *-- RTK Test CA, RCC SA Tambov, ...
* nChars = CertGetNameStringA(pCertContext, CERT_NAME_FRIENDLY_DISPLAY_TYPE, ;
* CERT_NAME_ISSUER_FLAG, 0, @m.szNameString, cchString)
If nChars > 1
=MessageBox("Найден сертификат: "+Chr(13)+;
szNameString,64,"Сообщение")
Else
lnResultCode = GetLastError()
=MessageBox("Невозможно получить имя субъета сертификата",16,"Ошибка: " + Transform(lnResultCode))
EXIT
EndIf
pCertContext = CertEnumCertificatesInStore(hStoreHandle, pCertContext)
EndDo
If !pCertContext = 0
= CertFreeCertificateContext(pCertContext)
EndIf
Else
lnResultCode = GetLastError()
=MessageBox("Невозможно открыть хранилище MY.",16,"Ошибка: " + Transform(lnResultCode))
EndIf
= CertCloseStore(hStoreHandle, 0)

У меня он замечательно отрабатывает!
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Влад Колосов

Сообщений: 22664
Откуда: Ростов-на-Дону
Дата регистрации: 05.05.2005
А вот этот код находит!


------------------
Совершенство - это не тогда, когда нельзя
ничего прибавить, а тогда, когда нечего убавить.
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Ну тогда попробуйте копипастить и выполнить еще раз код из 6-го поста сверху. Раньше этот код был не поправлен с учетом замечания Игоря Королёва. Сейчас я ошибку в коде специально поправил и он должен работать "как есть". Имена сертификатов данный код не выводит, но, по крайней мере, он должен писать, что "Сертификаты найдены!"... Проверьте, пожалуйста, ещё раз, чтобы точно убедиться! Неужели функция CertFindCertificateInStore на некоторых компьютерах не отрабатывает? Мне важно знать об этом...
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
dimuhametov

Сообщений: 1562
Откуда: Костанай
Дата регистрации: 01.11.2008
rvc44
Ну тогда попробуйте копипастить и выполнить еще раз код из 6-го поста сверху. Раньше этот код был не поправлен с учетом замечания Игоря Королёва. Сейчас я ошибку в коде специально поправил и он должен работать "как есть". Имена сертификатов данный код не выводит, но, по крайней мере, он должен писать, что "Сертификаты найдены!"... Проверьте, пожалуйста, ещё раз, чтобы точно убедиться! Неужели функция CertFindCertificateInStore на некоторых компьютерах не отрабатывает? Мне важно знать об этом...

Теперь оба алгоритма отработали на отлично ! Сертификаты найдены ! Спасибо.


------------------
Незнание делает жизнь такой интересной.
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Сегодня мне удалось, поколдовав немного с памятью, переложить на VFP код, определяющий для текущего пользователя (либо компьютера) имя криптопровайдера (CSP) по умолчанию:

#DEFINE PROV_GOST_94_DH 71
#DEFINE PROV_GOST_2001_DH 75
*-- Crypt{Get|Set}Provider
#DEFINE CRYPT_MACHINE_DEFAULT 0x00000001
#DEFINE CRYPT_USER_DEFAULT 0x00000002
*-- Здесь hттp://www.news2news.com/vfp/?function=827 некорректная декларация!
DECLARE INTEGER CryptGetDefaultProvider IN advapi32;
INTEGER dwProvType,;
INTEGER pdwReserved,;
INTEGER dwFlags,;
INTEGER pszProvName,;
INTEGER @ pcbProvName
DECLARE INTEGER GlobalAlloc IN kernel32;
INTEGER wFlags,;
INTEGER dwBytes
DECLARE INTEGER GlobalFree IN kernel32;
INTEGER hMem
*-- Получение имени криптопровайдера (CSP), определенного для компьютера по умолчанию
cbProvName = 0
*-- Получение длины имени провайдера по умолчанию
lnStatus = CryptGetDefaultProvider(PROV_GOST_2001_DH, 0, CRYPT_USER_DEFAULT, 0, @cbProvName)
If !lnStatus = 0
*-- hттp://msdn2.microsoft.com/en-us/library/aa366781.aspx -Memory Management Functions...
*-- Запрашиваем память у Windows и получаем указатель на неё (дескриптор)
*-- 0x0040 означает неперемещаемую (фиксированную) память, заполненную нулями
*-- cbProvName указывает размер выделяемой памяти в байтах
pbProvName = GlobalAlloc(0x0040, cbProvName)
*-- Получение имени провайдера по умолчанию.
lnStatus = CryptGetDefaultProvider(PROV_GOST_2001_DH, 0, CRYPT_USER_DEFAULT, pbProvName, @cbProvName)
If !lnStatus = 0
*-- Возвратим строку cRes, которая содержит cbProvName байт, начиная с адреса pbProvName:
cRes = SYS(2600, pbProvName, cbProvName)
=MessageBox("Имя криптопровайдера по умолчанию: "+cRes,64,"Сообщение")
Else
lnResultCode = GetLastError()
=MessageBox("Ошибка при определении имени провайдера по умолчанию!",16,"Ошибка: " +;
Transform(lnResultCode))
EndIf
*-- Возвращаем выделенный блок памяти Windows
GlobalFree(pbProvName)
Else
lnResultCode = GetLastError()
=MessageBox("Ошибка при определении длины имени провайдера по умолчанию!",16,"Ошибка: " +;
Transform(lnResultCode))
EndIf

У меня этот код посредством двойного обращения к API-функции CryptGetDefaultProvider сначала определяет длину имени криптопровайдера по умолчанию (в моём случае она составляет 60 символов), а затем и имя самого криптопровайдера:
Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider

Проверяйте, что возвращает этот код у вас...
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
Igor Korolyov

Сообщений: 34580
Дата регистрации: 28.05.2002
Это уже горе от ума. Не надо тут заморачиваться с ручным рулением памятью, фокс и сам прекрасно справится - декларация на сайте правильная. При том вполне логично попытаться за 1 вызов всё получить, а не дёргать 2 раза только чтобы сэкономить пару сотен байтиков

#DEFINE CRYPT_USER_DEFAULT 0x00000002
#DEFINE PROV_RSA_FULL 1
DECLARE INTEGER CryptGetDefaultProvider IN advapi32 ;
INTEGER, INTEGER, INTEGER, STRING @,INTEGER @
lnLen = 256
lcName = REPLICATE(CHR(0), m.lnLen)
IF CryptGetDefaultProvider(PROV_RSA_FULL, 0, CRYPT_USER_DEFAULT, @m.lcName, @m.lnLen) = 0
* buffer was too small, second attempt
lcName = REPLICATE(CHR(0), m.lnLen)
IF CryptGetDefaultProvider(PROV_RSA_FULL, 0, CRYPT_USER_DEFAULT, @m.lcName, @m.lnLen) = 0
MESSAGEBOX("Out of luck today ")
RETURN .F.
ENDIF
ENDIF
lcName = LEFT(m.lcName, m.lnLen-1)
? m.lcName

Естественно что "у нас" никаких PROV_GOST_* и в помине нету А вот RSA по идее в любой NT есть - под очень старыми, правда, будет чуток порезанный, а под новыми уже Strong.


------------------
WBR, Igor
Ratings: 0 negative/0 positive
Re: Подпись XML (СМЭВ)
rvc44
Автор

Сообщений: 2211
Откуда: Тамбов
Дата регистрации: 06.12.2005
Итак, собственно, добрались до кульминационного момента данной темы - формирования электронной подписи документа (ЭЦП). Зарегистрированные в системе идентификаторы объектов (OID'ы) алгоритмов находятся в следующем разделе реестра:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptDllFindOIDInfo
Подпись документа осуществляется при помощи функции API CryptSignMessage из crypt32.dll. Подписывать XML-файл будем отдельной от данных (detached) сигнатурой. Для тех, у кого загружен и установлен пакет Крипто-Про (загрузка и установка бесплатны), для СМЭВ в РФ можно и нужно использовать алгоритм с OID = '1.2.643.2.2.3'. При отсутствии пакета Крипто-Про можно использовать алгоритм цифровой подписи MD5 с OID = '1.2.840.113549.1.1.4'. 3-я цифра в OID: 643=РФ, 840=US. С наложением электронной подписи на документ на VFP у меня пока не всё "срослось", есть проблемы...
Обратите внимание на приведенное в коде определение структуры CRYPT_SIGN_MESSAGE_PARA и сформированную на основе её переменную SignPara. Не вполне понятно, что должно быть указано вместо PtrCertContext?
Декларация функции CryptSignMessage достаточно сложная и отличается от других тем, что её 4 и 5 параметры являются массивами, что в API-функциях встречается не часто. 4-й параметр rgpbToBeSigned представляет собой массив данных, передаваемых для формирования цифровой подписи, а 5-й параметр rgcbToBeSigned представляет собой массив размеров элементов переданного массива данных. Как раз с этими массивами, по моему, в VFP и возникают проблемы, поскольку API использут zero-based массивы, а VFP использует one-based массивы. Конечно, существует функция COMARRAY(oObject, 10+1000), но она применяется только для COM-объектов. Для массивов можно либо написать обёртку на VFP+JScript, способом показанным здесь: hттp://forum.foxclub.ru/read.php?29,271311 (пока не знаю, стоит ли это пробывать? уж что только не перепробывал!), либо можно создать пустой zero-based вариантный массив, как показано ниже, но похоже, этот способ исправления ситуации не подходит, поскольку создаваемый "объект" в контексте остального кода не уместен:
oScr = CreateObject("MSScriptControl.ScriptControl")
oScr.Language = "javascript"
oScr.Eval("DataArray = new Array()")
oScr.Eval("DataArray[0] = 'Test\n'")
oVarArray = oScr.Eval("DataArray")
Как ни крути, показанный ниже код формирования электронной подписи на VFP сырой и его нужно ещё допиливать и устранять баги. Буду благодарен за любые замечания, относящиеся к сути вопроса. Я как ни пытался запустить данный код - пока ничего не вышло. Надеюсь, найдутся на форуме люди, которые подтолкнут на путь истиный! ))
#DEFINE CERT_STORE_PROV_SYSTEM 10
#DEFINE CERT_COMPARE_NAME_STR_A 7
#DEFINE CERT_COMPARE_NAME_STR_W 8
#DEFINE CERT_COMPARE_SHIFT 16
#DEFINE CERT_INFO_SUBJECT_FLAG 7
#DEFINE PKCS_7_ASN_ENCODING 0x00010000
#DEFINE X509_ASN_ENCODING 0x00000001
#DEFINE CERT_FIND_ANY 0x00000000
#DEFINE CERT_FIND_HASH 0x00010000
#DEFINE CERT_FIND_SUBJECT_STR_A 0x00070007
#DEFINE CERT_FIND_SUBJECT_STR_W 0x00080007
#DEFINE CERT_FIND_SUBJECT_STR CERT_FIND_SUBJECT_STR_W
*-- Location of the system store:
#DEFINE CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000
#DEFINE CERT_SYSTEM_STORE_LOCATION_SHIFT 16
*-- Registry: HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE
#DEFINE CERT_SYSTEM_STORE_CURRENT_USER_ID 1
#DEFINE CERT_SYSTEM_STORE_LOCAL_MACHINE_ID 2
*-- 0x00010000 = (1 << 16) = (CERT_SYSTEM_STORE_CURRENT_USER_ID << CERT_SYSTEM_STORE_LOCATION_SHIFT)
#DEFINE CERT_SYSTEM_STORE_CURRENT_USER 0x00010000
#DEFINE CERT_NAME_EMAIL_TYPE 1
#DEFINE CERT_NAME_RDN_TYPE 2
#DEFINE CERT_NAME_ATTR_TYPE 3
#DEFINE CERT_NAME_SIMPLE_DISPLAY_TYPE 4
#DEFINE CERT_NAME_FRIENDLY_DISPLAY_TYPE 5
#DEFINE CERT_NAME_DNS_TYPE 6
#DEFINE CERT_NAME_URL_TYPE 7
#DEFINE CERT_NAME_UPN_TYPE 8
#DEFINE dnPROV_RSA_FULL 1
#DEFINE dnPROV_RSA_SIG 2
#DEFINE dnPROV_DSS 3
#DEFINE dnPROV_FORTEZZA 4
#DEFINE dnPROV_MS_EXCHANGE 5
#DEFINE dnPROV_SSL 6
*-- Из файла wincryptex.h из hттp://www.cryptopro.ru/CryptoPro/test/sample2_0.zip
#DEFINE PROV_RSA_FULL 1
#DEFINE PROV_RSA_SIG 2
#DEFINE PROV_DSS 3
#DEFINE PROV_FORTEZZA 4
#DEFINE PROV_MS_EXCHANGE 5
#DEFINE PROV_SSL 6
#DEFINE PROV_RSA_SCHANNEL 12
#DEFINE PROV_DSS_DH 13
#DEFINE PROV_EC_ECDSA_SIG 14
#DEFINE PROV_EC_ECNRA_SIG 15
#DEFINE PROV_EC_ECDSA_FULL 16
#DEFINE PROV_EC_ECNRA_FULL 17
#DEFINE PROV_SPYRUS_LYNKS 20
#DEFINE PROV_GOST_94_DH 71
#DEFINE PROV_GOST_2001_DH 75
*-- Crypt{Get|Set}Provider
#DEFINE CRYPT_MACHINE_DEFAULT 0x00000001
#DEFINE CRYPT_USER_DEFAULT 0x00000002
#DEFINE dnCRYPT_VERIFYCONTEXT 0xF0000000
#DEFINE HP_HASHVAL 0x0002
#DEFINE CERT_NAME_ISSUER_FLAG 0x1
#DEFINE CERT_NAME_DISABLE_IE4_UTF8_FLAG 0x00010000
#DEFINE MAX_NAME 256
#DEFINE dnALG_SID_GR3411 30
#DEFINE dnALG_CLASS_HASH BITLSHIFT(4,13) && (4 << 13)
#DEFINE dnALG_TYPE_ANY 0
#DEFINE dnCALG_GR3411 BITOR(BITOR(dnALG_CLASS_HASH,dnALG_TYPE_ANY),dnALG_SID_GR3411)
* Global Memory Flags
#DEFINE GMEM_FIXED 0x0000
#DEFINE GMEM_ZEROINIT 0x0040
DECLARE INTEGER CertOpenStore IN crypt32;
INTEGER lpszStoreProvider,;
LONG dwEncodingType,;
INTEGER hCryptProv,;
LONG dwFlags,;
STRING pvPara
DECLARE INTEGER CertFindCertificateInStore IN crypt32;
INTEGER hCertStore,;
LONG dwCertEncodingType,;
LONG dwFindFlags,;
LONG dwFindType,;
STRING pvFindPara,;
INTEGER pPrevCertContext
DECLARE INTEGER CertGetNameStringA IN crypt32;
INTEGER pCertContext,;
LONG dwType,;
LONG dwFlags,;
LONG pvTypePara,;
STRING @pszNameString,;
INTEGER cchNameString
* BOOL CryptSignMessage(PCRYPT_SIGN_MESSAGE_PARA pSignPara,
* BOOL fDetachedSignature,
* DWORD cToBeSigned,
* const BYTE* rgpbToBeSigned[],
* DWORD rgcbToBeSigned[],
* BYTE* pbSignedBlob,
* DWORD* pcbSignedBlob);
DECLARE INTEGER CryptSignMessage IN crypt32;
STRING @ pSignPara,;
INTEGER fDetachedSignature,;
INTEGER cToBeSigned,;
STRING @ rgpbToBeSigned,;
LONG @ rgcbToBeSigned,;
STRING @ pbSignedBlob,;
LONG @ pcbSignedBlob
DECLARE INTEGER CryptReleaseContext IN advapi32;
INTEGER hProvHandle,;
INTEGER nReserved
DECLARE INTEGER CertFreeCertificateContext IN crypt32;
INTEGER pCertContext
DECLARE INTEGER CertCloseStore IN crypt32;
INTEGER hCertStore,;
LONG dwFlag
DECLARE INTEGER GetLastError ;
IN WIN32API AS GetLastError
DECLARE INTEGER GlobalAlloc IN kernel32;
INTEGER wFlags,;
INTEGER dwBytes
DECLARE INTEGER GlobalFree IN kernel32;
INTEGER hMem
DECLARE RtlMoveMemory IN kernel32 As Str2Mem;
INTEGER, STRING @, INTEGER
LOCAL lCryptoPro, lnLen, lcName, lnStoreProvider, lcPara, hStoreHandle, pCertContext, lnResultCode, ;
lnSize, lcMessage, lcFindPara, lcOID, lnOID
*-- Открываем хранилище сертификатов
lnStoreProvider = CERT_STORE_PROV_SYSTEM
lcPara = STRCONV("MY" + CHR(0), 5)
hStoreHandle = CertOpenStore(lnStoreProvider, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, lcPara)
If !hStoreHandle = 0
*-- Получаем указатель на наш сертификат
pCertContext = 0
*-- Найдем заданный сертификат по его CN, доступному для просмотра в оснастке certmgr.msc
lcFindPara = STRCONV("CN своего сертификата (ключа)" + CHR(0), 5)
lnPrevCertContext = 0
pCertContext = CertFindCertificateInStore(hStoreHandle,;
BITOR(PKCS_7_ASN_ENCODING, X509_ASN_ENCODING),;
0, CERT_FIND_SUBJECT_STR, lcFindPara, lnPrevCertContext)
If !pCertContext = 0
*--Сертификат был найден. Получим и выведем имя субъекта сертификата.
szNameString = Replicate(Chr(0), MAX_NAME)
cchString = MAX_NAME && 256
nChars = CertGetNameStringA(pCertContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, 0, @m.szNameString, cchString)
If nChars > 1
=MessageBox("Хранилище сертификатов MY открылось успешно!"+Chr(13)+;
"Найден сертификат:"+Chr(13)+;
szNameString,64,"Сообщение")
************************************************************************
* Алгоритм цифровой подписи ГОСТ Р 34.10-2001 (ГОСТ Р 34.10-94/ГОСТ Р 34.11-94 по схеме Эльгамаля)
lcOID = '1.2.643.2.2.3' && szOID_CP_GOST_R3411_R3410EL
* ДЛЯ ТЕХ, У КОГО НЕТ КРИПТО_ПРО МОЖНО ИСПОЛЬЗОВАТЬ: Алгоритм цифровой подписи MD5
* lcOID = '1.2.840.113549.1.1.4' && szOID_RSA_MD5 или szOID_RSA_MD5RSA
* Выделим блок памяти под "Object identifier (OID) of an algorithm"
lnOID = GlobalAlloc(0x0000, Len(lcOID)) && GMEM_FIXED = 0x0000
* Копируем строковые данные по адресу выделенного блока памяти
= Str2Mem(lnOID, @lcOID, Len(lcOID))
* Структура CRYPT_ALGORITHM_IDENTIFIER определана в MSDN:
* hттp://msdn.microsoft.com/en-us/library/aa923698.aspx
*|typedef struct _CRYPT_ALGORITHM_IDENTIFIER {
*| LPSTR pszObjId; 0:4
*| CRYPT_OBJID_BLOB Parameters; 4:8
*|} CRYPT_ALGORITHM_IDENTIFIER, *PCRYPT_ALGORITHM_IDENTIFIER; total 12 bytes
* Структура CRYPT_SIGN_MESSAGE_PARA определана в MSDN:
* hттp://msdn.microsoft.com/en-us/library/windows/desktop/aa381468%28v=vs.85%29.aspx
*| typedef struct _CRYPT_SIGN_MESSAGE_PARA {
*| DWORD cbSize; 0:4 Размер этой структуры в байтах
*| DWORD dwMsgEncodingType; 4:4 Type of encoding used: BITOR(PKCS_7_ASN_ENCODING, X509_ASN_ENCODING)
*| PCCERT_CONTEXT pSigningCert; 8:4 Указатель на структуру CERT_CONTEXT
*| CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm; 12:12
*| void *pvHashAuxInfo; 24:4
*| DWORD cMsgCert; 28:4 Number of elements in the rgpMsgCert array of CERT_CONTEXT structures.
*| PCCERT_CONTEXT *rgpMsgCert; 32:4 Указатель на структуру CERT_CONTEXT
*| DWORD cMsgCrl; 36:4
*| PCCRL_CONTEXT *rgpMsgCrl; 40:4 Указатель на структуру CRL_CONTEXT
*| DWORD cAuthAttr; 44:4
*| PCRYPT_ATTRIBUTE rgAuthAttr; 48:4 Указатель на структуру CRYPT_ATTRIBUTE
*| DWORD cUnauthAttr; 52:4
*| PCRYPT_ATTRIBUTE rgUnauthAttr; 56:4 Указатель на структуру CRYPT_ATTRIBUTE
*| DWORD dwFlags; 60:4
*| DWORD dwInnerContentType; 64:4
*| #ifdef CRYPT_SIGN_MESSAGE_PARA_HAS_CMS_FIELDS
*| CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm; 68:12
*| void *pvHashEncryptionAuxInfo; 80:4
*| #endif
*| } CRYPT_SIGN_MESSAGE_PARA, *PCRYPT_SIGN_MESSAGE_PARA; total 84 bytes
* Подтверждение, что размер структуры 84 байта здесь: hттp://www.cryptopro.ru/forum2/default.aspx?g=posts&t=3200
* Структура CERT_CONTEXT определана вот так в MSDN:
* hттp://msdn.microsoft.com/en-us/library/aa919756.aspx
*|typedef struct _CERT_CONTEXT {
*| DWORD dwCertEncodingType; 0:4
*| BYTE* pbCertEncoded; 4:4 Указатель имеет размер в 4 байта
*| DWORD cbCertEncoded; 8:4
*| PCERT_INFO pCertInfo; 12:4 Указатель на структуру CERT_INFO
*| HCERTSTORE hCertStore; 16:4 Указатель
*|} CERT_CONTEXT, *PCERT_CONTEXT; total 20 bytes
*|typedef const CERT_CONTEXT *PCCERT_CONTEXT;
lnLen = 84 && Длина структуры CRYPT_SIGN_MESSAGE_PARA
cMsgCert = 1
PtrCertContext = pCertContext && @pCertContext
SignPara = BINTOC(lnLen,"4RS") +; && Size of this structure in bytes
BINTOC(BITOR(PKCS_7_ASN_ENCODING, X509_ASN_ENCODING),"4RS") +; && Type of encoding used
BINTOC(pCertContext,"4RS") +; && A pointer to the CERT_CONTEXT to be used in the signing
BINTOC(lnOID,"4RS") +; && CRYPT_ALGORITHM_IDENTIFIER containing the hashing algorithm used to hash the data to be signed
REPLICATE(CHR(0), 12) +;
BINTOC(cMsgCert,"4RS") +; && Number of elements in the rgpMsgCert array of CERT_CONTEXT structures
BINTOC(PtrCertContext,"4RS") +; && ?If the pSigningCert is to be included, a pointer to it must be in the rgpMsgCert array
REPLICATE(CHR(0), lnLen-36)
* Возможно попробывать так? hттp://www.foxite.com/archives/byte-datatype-in-foxpro-0000234613.htm
* Byte datatype in FOXPRO:
* DIMENSION DataArray[4] AS Byte
* DataArray[1] = 0x54 && 'T'
* DataArray[2] = 0x65 && 'e'
* DataArray[3] = 0x73 && 's'
* DataArray[4] = 0x74 && 't'
* COMARRAY(yourReaderObject, 0) && Call COMARRAY() to specify array is a zero-based and passed by value
* IF yourReaderObject.TheMethodThatRequiresByteArray(@DataArray) && This is how you pass the byte array, use @
* && Successful
* ENDIF
* Q: I need to send data in byte datatype. Anyone can tell me how I can convert my data from string or number type into byte datatype in vfp9?
* A: For numbers, check out BINTOC function. String can be send as such or by adding CHR(0) to it.
lcString = 'Test' && Строка для подписывания её ЭЦП. Или: 'ВАО Интурист - Туроператор N1'
DIMENSION DataArray[1]
DataArray[1] = lcString
lnStrLen = 4 && Len(lcString)
DIMENSION SizeArray[1]
SizeArray[1] = lnStrLen
lDSig = .T.
lnLen = 256
lcSignature = REPLICATE(CHR(0), m.lnLen)
If CryptSignMessage(@SignPara, 1, 1, @DataArray, @SizeArray, @m.lcSignature, @m.lnLen) = 0
* buffer was too small, second attempt
lcSignature = REPLICATE(CHR(0), m.lnLen)
If CryptSignMessage(@SignPara, 1, 1, @DataArray, @SizeArray, @m.lcSignature, @m.lnLen) = 0
=MessageBox("ЭЦП не была сгенерирована!",16,"Ошибка")
lDSig = .F.
EndIf
EndIf
If lDSig
=MessageBox("ЭЦП сгенерирована успешно!",64,"Сообщение")
lcSignature = LEFT(m.lcSignature, m.lnLen-1)
? m.lcSignature
EndIf
*-- Возвращаем выделенный блок памяти Windows
GlobalFree(lnOID)
************************************************************************
Else
lnResultCode = GetLastError()
=MessageBox("Getting the signer name failed",16,"Ошибка: " + Transform(lnResultCode))
EndIf
= CertFreeCertificateContext(pCertContext)
Else
lnResultCode = GetLastError()
=MessageBox("Хранилище сертификатов MY открылось, однако,"+Chr(13)+;
"обнаружить сертификаты не удалось!",16,"Ошибка: " + Transform(lnResultCode))
EndIf
Else
lnResultCode = GetLastError()
=MessageBox("Невозможно открыть хранилище MY.",16,"Ошибка: " + Transform(lnResultCode))
EndIf
= CertCloseStore(hStoreHandle, 0)
Не забудьте поменять "CN своего сертификата (ключа)" на реальное значение (как правило, оно совпадает с именем файла *.cer). Вызов CryptSignMessage должен быть двойным. На первом вы запрашиваете размер подписи, потом выделяете память, и лишь на повторном вызове сможете получить заветную подпись (для СМЭВ в РФ это ГОСТ Р 34.10-2001, по схеме Эльгамаля).



Исправлено 2 раз(а). Последнее : rvc44, 16.07.12 20:01
Ratings: 0 negative/0 positive


Извините, только зарегистрированные пользователи могут оставлять сообщения в этом форуме.

On-line: 44 (Гостей: 44)

© 2000-2024 Fox Club 
Яндекс.Метрика