From 9a5d957a8833f94ce6115dac82e7efd758d02e1a Mon Sep 17 00:00:00 2001 From: Dmitrii Evdokimov Date: Thu, 27 Jun 2024 03:30:59 +0300 Subject: [PATCH] Replace System.Security.Cryptography.Pkcs by own ASN1 --- Api5704/ASN1.cs | 438 +++++++++++++++++++++++++++++++++++++++++ Api5704/Api5704.csproj | 6 +- Api5704/ApiExtra.cs | 24 ++- Api5704/ApiHelper.cs | 6 +- Api5704/Config.cs | 4 + Api5704/PKCS7.cs | 10 +- Api5704/Program.cs | 19 +- README.md | 36 +++- 8 files changed, 514 insertions(+), 29 deletions(-) create mode 100644 Api5704/ASN1.cs diff --git a/Api5704/ASN1.cs b/Api5704/ASN1.cs new file mode 100644 index 0000000..bf19e99 --- /dev/null +++ b/Api5704/ASN1.cs @@ -0,0 +1,438 @@ +#region License +/* +Copyright 2024 Dmitrii Evdokimov +Open source software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion + +using System.Text; + +namespace Api5704; + +public static class ASN1 +{ + public static int BufferSize { get; set; } = 4096; + + public static async Task CleanSignAsync(byte[] signedData) + { + try + { + using var stream = new MemoryStream(signedData); + using var reader = new BinaryReader(stream); + + // type 0x30, length 0x80 or 1..4 bytes additionally + SkipTypeLength(); + + // type 0x06 length 0x09 data... - ObjectIdentifier (signedData "1.2.840.113549.1.7.2") + if (!ReadOid(0x02)) + return []; + + // 0xA0 0x80 0xA0 0x80 + SkipTypeLength(2); + + // 0x02 0x01 0x01 - Integer (version 1) + if (!ReadVersion()) + return []; + + // 0x31 ... - list of used algoritms + SkipTypeLengthData(); + + // 0x30 0x80 + SkipTypeLength(); + + // type 0x06 length 0x09 data... - ObjectIdentifier (data "1.2.840.113549.1.7.1") + if (!ReadOid(0x01)) + return []; + + // 0xA0 0x80 0x24 0x80 + SkipTypeLength(2); + + // type 0x04 - OctetString + if (reader.ReadByte() != 0x04) + return []; + + // length of enclosed data (long or undefined) + var len = ReadLength(); + + if (len is null) // undefined + { + var start = stream.Position; + var end = Seek(stream, [0x00, 0x00]); + + len = end - start; + stream.Position = start; + } + + // start of enclosed data + using var output = new MemoryStream(); + await stream.CopyToAsync(output); //TODO copy len bytes only + output.SetLength((long)len); // truncate tail + + return output.ToArray(); + + #region local functions + // 1..5 bytes + long? ReadLength() + { + byte b = reader.ReadByte(); + + // undefined: end by 0x00 0x00 bytes + if (b == 0x80) + return null; + + // 1 next byte: 128..255 + if (b == 0x81) + { + var v = reader.ReadByte(); + return v; + } + + // 2 next bytes: 256..65535 + if (b == 0x82) + { + var v = reader.ReadBytes(2); + return + v[0] * 0x100 + + v[1]; + } + + // 3 next bytes: 65536..16777215 + if (b == 0x83) + { + var v = reader.ReadBytes(3); + return + v[0] * 0x10000 + + v[1] * 0x100 + + v[2]; + } + + // 4 next bytes, 2 standards: + // 1 .. 4 294 967 295 + // 16 777 216 .. 4 294 967 295 (4 Gb) + if (b == 0x84) + { + var v = reader.ReadBytes(4); + return + v[0] * 0x1000000 + + v[1] * 0x10000 + + v[2] * 0x100 + + v[3]; + } + + // this byte: 0..127 + else + return b; + } + + // 06 09 then + // 2A 86 48 86 F7 0D 01 07 02 - oid 1.2.840.113549.1.7.2 "signedData" + // 2A 86 48 86 F7 0D 01 07 01 - oid 1.2.840.113549.1.7.1 "data" + bool ReadOid(byte n) + { + var b = reader.ReadBytes(11); + int i = 0; + return + b[i++] == 0x06 && // type 06 => ObjectIdentifier + b[i++] == 0x09 && // length => 9 bytes + + b[i++] == 0x2A && // data ... + b[i++] == 0x86 && + b[i++] == 0x48 && + b[i++] == 0x86 && + b[i++] == 0xF7 && + b[i++] == 0x0D && + b[i++] == 0x01 && + b[i++] == 0x07 && + b[i++] == n; + } + + // 02 01 01 + bool ReadVersion() + { + var b = reader.ReadBytes(3); + int i = 0; + return + b[i++] == 0x02 && // type 02 => Integer + b[i++] == 0x01 && // length => 1 byte + b[i++] == 0x01; // data (1 => version 1) + } + + // skip type and length + // 30 80 + // 02 01 + void SkipTypeLength(int n = 1) + { + for (int i = 0; i < n; i++) + { + //type + stream.Position++; + + //length + byte b = reader.ReadByte(); + + if (b > 0x80) + { + stream.Position += b - 0x80; + } + } + } + + // skip type, length and data by this length + // 02 01 01 + void SkipTypeLengthData() + { + // type + stream.Position++; + + // length + var len = ReadLength(); + + //data + if (len is null) // undefined + { + var end = Seek(stream, [0x00, 0x00]); + stream.Position = end + 2; + } + else + { + stream.Position += (long)len; + } + } + #endregion local functions + } + catch + { + return []; + } + } + + // https://keestalkstech.com/2010/11/seek-position-of-a-string-in-a-file-or-filestream/ + // Written by Kees C. Bakker, updated on 2022-09-18 + + /* EXAMPLE: + var url = "https://keestalkstech.com/wp-content/uploads/2020/06/photo-with-xmp.jpg?1"; + + using var client = new HttpClient(); + using var downloadStream = await client.GetStreamAsync(url); + + using var stream = new MemoryStream(); + await downloadStream.CopyToAsync(stream); + + stream.Position = 0; + var enc = Encoding.UTF8; + var start = Seek(stream, " ro = buffer; + + if (r < size) + ro = ro[..(offset + size)]; + + // check if we can find our search bytes in the buffer + var i = ro.IndexOf(search); + + if (i > -1) + return position + i; + + // when less then size was read, we are done and found nothing + if (r < size) + return -1; + + // we still have bytes to read, so copy the last search + // length to the beginning of the buffer. It might contain + // a part of the bytes we need to search for + + offset = search.Length; + size = bufferSize - offset; + Array.Copy(buffer, buffer.Length - offset, buffer, 0, offset); + position += bufferSize - offset; + } + } + + public static byte[] Oid(string oid) + { + string[] parts = oid.Split('.'); + MemoryStream ms = new(); + + //0 + int x = int.Parse(parts[0]); + + //1 + int y = int.Parse(parts[1]); + ms.WriteByte((byte)(40 * x + y)); + + //2+ + for (int i = 2; i < parts.Length; i++) + { + string part = parts[i]; + int octet = int.Parse(part); + byte[] b = EncodeOctet(octet); + ms.Write(b, 0, b.Length); + } + + return ms.ToArray(); + } + + public static string Oid(byte[] oid) + { + StringBuilder sb = new(); + + // Pick apart the OID + int x = oid[0] / 40; + int y = oid[0] % 40; + + if (x > 2) + { + // Handle special case for large y if x = 2 + y += (x - 2) * 40; + x = 2; + } + + sb.Append(x).Append('.').Append(y); + long val = 0; + + for (int i = 1; i < oid.Length; i++) + { + val = (val << 7) | ((byte)(oid[i] & 0x7F)); + + if ((oid[i] & 0x80) != 0x80) + { + sb.Append('.').Append(val); + val = 0; + } + } + + return sb.ToString(); + } + + public static byte[] EncodeOctet(int value) + { + /* + For example, the OID value is 19200300. + + 1. Convert 19200300 to Hex 0x124F92C + 2. 0x124F92C & 0x7F = 0x2C -- Last Byte + 3. ((0x124F92C >> 7) & 0x7F) | 0x80 = 0xF2 --- 3rd Byte + 4. ((0x124F92C >> 14) & 0x7F) | 0x80 = 0x93 ---- 2nd Byte + 5. ((0x124F92C >> 21) & 0x7F) | 0x80 = 0x89 ----- 1st Byte + + So after encoding, it becomes 0x89 0x93 0xF2 0x2C. + */ + + uint x = (uint)value; + + if (x > 0xFFFFFF) // 4 Bytes + { + var b = new byte[4]; + + b[0] = Convert.ToByte((x >> 21) & 0x7F | 0x80); + b[1] = Convert.ToByte((x >> 14) & 0x7F | 0x80); + b[2] = Convert.ToByte((x >> 7) & 0x7F | 0x80); + b[3] = Convert.ToByte( x & 0x7F); + + return b; + } + + if (x > 0xFFFF) // 3 Bytes + { + var b = new byte[3]; + + b[0] = Convert.ToByte((x >> 14) & 0x7F | 0x80); + b[1] = Convert.ToByte((x >> 7) & 0x7F | 0x80); + b[2] = Convert.ToByte( x & 0x7F); + + return b; + } + + if (x > 127) // 2 Bytes + { + var b = new byte[2]; + + b[0] = Convert.ToByte((x >> 7) & 0x7F | 0x80); + b[1] = Convert.ToByte( x & 0x7F); + + return b; + } + + // (x < 127) // 1 Byte + return [Convert.ToByte(x)]; + } +} + +/* +using System.Security.Cryptography.Pkcs; + +/// +/// Извлечь из PKCS#7 с ЭП чистый исходный текст. +/// Криптопровайдер и проверка ЭП здесь не используются - только извлечение блока данных из формата ASN.1 +/// +/// Массив байтов с сообщением в формате PKCS#7. +/// Массив байтов с исходным сообщением без ЭП. +public static byte[] CleanSign(byte[] data) +{ + var signedCms = new SignedCms(); + signedCms.Decode(data); + + return signedCms.ContentInfo.Content; +} + +/// +/// Извлечь из файла PKCS#7 с ЭП чистый исходный файл. +/// Криптопровайдер и проверка ЭП здесь не используются - только извлечение блока данных из формата ASN.1 +/// +/// Исходный файл. +/// Файл с результатом. +/// +public static async Task CleanSignAsync(string src, string dst) +{ + byte[] data = await File.ReadAllBytesAsync(src); + byte[] data2 = CleanSign(data); + await File.WriteAllBytesAsync(dst, data2); +} +*/ diff --git a/Api5704/Api5704.csproj b/Api5704/Api5704.csproj index 3f126d2..7cc7b9e 100644 --- a/Api5704/Api5704.csproj +++ b/Api5704/Api5704.csproj @@ -6,7 +6,7 @@ Api5704 enable enable - 8.2024.626 + 8.2024.627 diev 2022-2024 Dmitrii Evdokimov Предоставление ССП при обращении пользователя к НБКИ как КБКИ-контрагенту в режиме «одного окна» по Указанию Банка России № 5704-У. @@ -25,8 +25,4 @@ embedded - - - - diff --git a/Api5704/ApiExtra.cs b/Api5704/ApiExtra.cs index 5db9327..ff72739 100644 --- a/Api5704/ApiExtra.cs +++ b/Api5704/ApiExtra.cs @@ -28,14 +28,16 @@ public static class ApiExtra /// Пакетная обработка папок с запросами (Extra). /// /// Папка с исходными запросами. - /// Папка с готовыми запросами. - /// Папка с готовыми результатами. - /// Папка с готовыми ответами. + /// Папка с отправленными запросами. + /// Папка с полученными квитанциями. + /// Папка с полученными сведениями. public static async Task PostRequestFolderAsync(string dir, string requests, string results, string answers) { - Directory.CreateDirectory(requests); - Directory.CreateDirectory(results); - Directory.CreateDirectory(answers); + if (!requests.Equals(string.Empty)) Directory.CreateDirectory(requests); + if (!results.Equals(string.Empty)) Directory.CreateDirectory(results); + if (!answers.Equals(string.Empty)) Directory.CreateDirectory(answers); + + int count = 0; foreach (var file in Directory.GetFiles(dir, "*.xml")) { @@ -43,7 +45,8 @@ public static async Task PostRequestFolderAsync(string dir, string requests, str if (data[0] == 0x30) { - data = PKCS7.CleanSign(data); + //data = PKCS7.CleanSign(data); + data = await ASN1.CleanSignAsync(data); } XmlDocument doc = new(); @@ -51,7 +54,7 @@ public static async Task PostRequestFolderAsync(string dir, string requests, str string id = doc.DocumentElement!.GetAttribute("ИдентификаторЗапроса"); string date = DateTime.Now.ToString("yyyy-MM-dd"); - string name = $"{date}.{id}"; + string name = $"{Path.GetFileName(file)}.{date}.{id}"; string request = Path.Combine(results, name + ".request.xml"); string result = Path.Combine(results, name + ".result.xml"); @@ -64,9 +67,14 @@ public static async Task PostRequestFolderAsync(string dir, string requests, str if (File.Exists(answer)) { File.Delete(file); + count++; } Thread.Sleep(1000); } + + Console.WriteLine($"Сведений получено: {count}."); + + Environment.Exit(0); } } diff --git a/Api5704/ApiHelper.cs b/Api5704/ApiHelper.cs index d81defb..62f96a8 100644 --- a/Api5704/ApiHelper.cs +++ b/Api5704/ApiHelper.cs @@ -54,7 +54,8 @@ public class ApiHelper await File.WriteAllBytesAsync(file + ".sig", data); // Clean data - data = PKCS7.CleanSign(data); + //data = PKCS7.CleanSign(data); + data = await ASN1.CleanSignAsync(data); // Write clean XML await File.WriteAllBytesAsync(file, data); @@ -65,7 +66,8 @@ public class ApiHelper await File.WriteAllBytesAsync(file, data); // Clean data - data = PKCS7.CleanSign(data); + //data = PKCS7.CleanSign(data); + data = await ASN1.CleanSignAsync(data); } string content = Encoding.UTF8.GetString(data); diff --git a/Api5704/Config.cs b/Api5704/Config.cs index 93aebe9..54b2140 100644 --- a/Api5704/Config.cs +++ b/Api5704/Config.cs @@ -33,6 +33,10 @@ public class Config public bool SignFile { get; set; } = true; public bool CleanSign { get; set; } = true; public int MaxRetries { get; set; } = 10; + public string DirSources { get; set; } = string.Empty; + public string DirRequests { get; set; } = string.Empty; + public string DirResults { get; set; } = string.Empty; + public string DirAnswers { get; set; } = string.Empty; public string CspTest { get; set; } = @"C:\Program Files\Crypto Pro\CSP\csptest.exe"; public string CspTestSignFile { get; set; } = diff --git a/Api5704/PKCS7.cs b/Api5704/PKCS7.cs index 55d3897..ede159b 100644 --- a/Api5704/PKCS7.cs +++ b/Api5704/PKCS7.cs @@ -32,12 +32,14 @@ internal static class PKCS7 /// /// Массив байтов с сообщением в формате PKCS#7. /// Массив байтов с исходным сообщением без ЭП. - public static byte[] CleanSign(byte[] data) + public static async Task CleanSign(byte[] data) { - var signedCms = new SignedCms(); - signedCms.Decode(data); + //var signedCms = new SignedCms(); + //signedCms.Decode(data); - return signedCms.ContentInfo.Content; + //return signedCms.ContentInfo.Content; + + return await ASN1.CleanSignAsync(data); } /// diff --git a/Api5704/Program.cs b/Api5704/Program.cs index 0a7cbef..cf4c52d 100644 --- a/Api5704/Program.cs +++ b/Api5704/Program.cs @@ -36,7 +36,23 @@ static async Task Main(string[] args) Environment.Exit(2); } - if (args.Length == 0) Usage(); + if (args.Length == 0) + { + string sources = Config.DirSources; + + if (!string.IsNullOrEmpty(sources) && Directory.Exists(sources)) + { + Console.WriteLine(@$"Параметры не указаны, но есть папка ""{sources}""."); + // dir requests results answers + await ApiExtra.PostRequestFolderAsync(sources, + Config.DirRequests, Config.DirResults, Config.DirAnswers); + } + else + { + Usage(); + } + } + string cmd = args[0].ToLower(); try @@ -135,6 +151,7 @@ result.xml answer.xml Параметры: request.xml result.xml answer.xml dir - пакетная обработка запросов (auto) из папки. + Это действие по умолчанию, если параметров не указано, но есть папка DirSources в конфиге. Параметры: sources requests results answers"; diff --git a/README.md b/README.md index b8d0584..9dae3e2 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,8 @@ ## Config При первом запуске будет создан файл настроек `Api5704.config.json` -с параметрами по умолчанию. Откорректируйте его перед новым запуском: +(в папке с программой) с параметрами по умолчанию. +Откорректируйте его перед новым запуском: * `MyThumbprint` = отпечаток вашего сертификата для его выбора в Хранилище; * `VerboseClient` = отображать содержимое вашего сертификата; @@ -76,6 +77,14 @@ * `ProxyAddress` = url прокси-сервера (опционально); * `SignFile` = подписывать запросы в программе; * `CleanSign` = удалять подписи ответов в программе; +* `MaxRetries` = число попыток с предписанным интервалом в 1 сек., +чтобы получить сведения по запросу; +* `DirSources` = папка с исходными запросами для пакетной обработки +(должна существовать, чтобы при запуске без параметров, файлы брались +оттуда; +* `DirRequests` = папка с отправленными запросами; +* `DirResults` = папка с полученными квитанциями; +* `DirAnswers` = папка с полученными сведениями. * `CspTest` = путь к программе КриптоПро `csptest.exe` (опционально); * `CspTestSignFile` = команда с параметрами для подписи запросов в программе, где: @@ -174,21 +183,26 @@ Api5704 AUTO request.xml result.xml answer.xml Пакетная обработка запросов (`auto`) из папки за один запуск - -команда `dir`: +команда `dir` (это и действие по умолчанию, если параметров не указано +вовсе, но есть папка `DirSources` в конфиге, а также там указаны папки +`DirRequests`, `DirResults`, `DirAnswers`): Api5704 DIR sources requests results answers + Api5704 где: -- `sources` - папка с исходными запросами `*.xml` (имена файлов любые); +- `sources` - папка с исходными запросами `*.xml` (имена файлов любые - +рекомендуется использовать ФИО); - `requests` - папка, куда будут сложены копии исходных файлов, -переименованные по маске `yyyy-MM-dd.guid.request.xml` +переименованные по маске `ФИО.yyyy-MM-dd.guid.request.xml`: + - `ФИО` - исходное имя файла (например, ФИО), - `yyyy-MM-dd` - текущая дата, - `guid` - ИдентификаторЗапроса из XML; -- `results` - папка, куда будут сложены квитанции, -переименованные по аналогичной маске `yyyy-MM-dd.guid.result.xml`; -- `answers` - папка, куда будут сложены квитанции, -переименованные по аналогичной маске `yyyy-MM-dd.guid.answer.xml`. +- `results` - папка, куда будут сложены полученные квитанции, +переименованные по аналогичной маске `ФИО.yyyy-MM-dd.guid.result.xml`; +- `answers` - папка, куда будут сложены полученные сведения, +переименованные по аналогичной маске `ФИО.yyyy-MM-dd.guid.answer.xml`. После получения файла в папке `answers`, соответствующий ему исходный файл будет считаться обработанным и удален из папки `sources`, при этом @@ -212,9 +226,13 @@ Номер версии программы указывается по нарастающему принципу: * Требуемая версия .NET (8); -* Год разработки (2024); +* Год текущей разработки (2024); * Месяц без первого нуля и день редакции (624 - 24.06.2024); * Номер билда - просто нарастающее число для внутренних отличий. +Если настроен сервис AppVeyor, то это его автоинкремент. + +Продукт развивается для собственных нужд, и поэтому +Breaking Changes могут случаться чаще, чем это в SemVer. ## License