Visual Basic, .NET, ASP, VBScript
 

   
 
Описание для автора не найдено
 
     
   
 
Понимание цифровой подписи XML
Автор: Rich Salz, DataPower Technology
Перевод: Шатохина Надежда(sna@uneta.org), UNETA (http://www.uneta.org/)
Июль 2003
Применяется к:
  • Спецификации Web-сервисов (спецификация WS-Security и др.)
  • Microsoft .NET Framework
  • Обзор:
    В этой статье рассматривается спецификация Цифровая подпись XML (XML Digital Signature), объясняется ее модель обработки и некоторые возможности. Представлено более детальное, глубокое понимание того, как спецификация WS-Security реализовывает возможность обеспечения безопасности сообщений.
    Содерджание:
  • Введение
  • Криптография цифровой подписи без математики
  • Формат подписи
  • Атрибут ds:Signature/@Id
  • Элемент ds:SignatureValue
  • Элемент ds:Signature/ds:Object
  • Элемент ds:SignedInfo
  • Элемент ds:Reference
  • Элемент ds:KeyInfo
  • Заключение
  • Введение

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

    SSL/TLS также обеспечивает целостность сообщения (а также конфиденциальность сообщения), но он делает это только во время транспортировки сообщения. Как только сообщение получено сервером (или, более обобщенно, основным получателем), чтобы можно было его обработать, защита SSL должна быть снята.

    Как более тонкий вопрос, SSL работает только между конечными точками линии связи. Если я разрабатываю новый Web-сервис и в качестве шлюза использую стандартный HTTP-сервер (такой как IIS или Apache), или если я устанавливаю связь с большим предприятием, которое использует SSL-акселераторы, целостность сообщения сохраняется только до тех пор, пока не будет прервано SSL-соединение.

    Для аналогии рассмотрим обычное письмо. Если я посылаю чек моей телефонной компании, я подписываю чек-сообщение и помещаю его в конверт, таким образом, обеспечивая конфиденциальность и доставку. При получении почты телефонная компания удаляет конверт, выбрасывает его и затем обрабатывает чек. Я мог бы сделать свое сообщение частью конверта, например, наклеив платеж на почтовую открытку, но это было бы глупо.

    XML-подпись должна определяться серией встраиваемых XML-элементов или же присоединяться к любому XML-документу. Это позволило бы удостоверить получателя в том, что сообщение не было изменено с момента отправки.

    Спецификация Синтаксис и обработка XML-подписи (XML-Signature Syntax and Processing, в этой статье используется аббревиатура XML DSIG) была совместной разработкой W3C и IETF. Она стала официальной Рекомендацией W3C с февраля 2002 года. Доступны многие реализации; в .NET Framework ее реализовывает System.Security.Cryptography.Xml. В триаде WS-Security— аутентификация, целостность содержимого и его конфиденциальность — XML DSIG обеспечивает целостность и может использоваться для аутентификации отправителя.

    Криптография цифровой подписи без математики

    До того как мы по-настоящему сможем понять XML DSIG, нам надо постичь некоторые основы криптографии. В этом разделе я рассмотрю основные ее принципы, но без паники: не будет никакой сложной математики.

    Цифровая подпись обеспечивает контроль целостности некоторого содержимого. Если хотя бы один байт исходного содержимого был изменен — к цене добавляется дополнительный ноль, «2» меняется на «4» или «Нет» на «Да», и т.д. — тогда подпись не пройдет проверку на достоверность. Вот как все это работает.

    Первый шаг — хэширование сообщения. Криптографический хэш выбирает произвольный поток байт и преобразовывает его в отдельное значение фиксированного размера, известное как дайджест (digest) или хэш. Создание дайджеста — однонаправленный процесс: невозможно вычислить и воссоздать сообщение из хэша или найти два разных сообщения, которые производят одинаковое цифровое значение.

    Самый распространенный механизм хэширования — SHA1, Безопасный алгоритм хэширования (Secure Hash Algorithm). Он был создан правительством США и выпущен в качестве стандарта в 1995; полная спецификация доступна по адресу http://www.itl.nist.gov/fipspubs/fip180-1.htm. SHA1 берет любое сообщение длиной до 264 байта и создает 20-байтовый результат. (Это значит, что существует 2160 возможных цифровых значений; для сравнения, по текущим оценкам количество протонов во вселенной составляет около 2250).

    Итак, я генерирую сообщение М и создаю хэш (выражение «хэш сообщения М» записывается H(M)), вы получаете M и H(M) и можете создать собственный хэш H'(M). Если эти два значения (H(M) и H'(M)) совпадают, мы знаем, что вы получили то, что я отправлял. Чтобы защитить M от изменений, мне надо только защитить от изменений H(M).

    Как это сделать? Существует два подхода. Первый — поместить общий секрет в хэш. Иначе говоря, создать H(S+M) (где S — секрет). Когда вы получаете сообщение, для создания H'(S+M) вы используете собственную копию S. Этот новый хэш называется HMAC или Код аутентичности хэшированного сообщения (Hashed Message Authentication Code).

    Когда мы используем HMAC, прочность защиты целостности зависит от невозможности разгадки S. Поэтому S должен быть чем-то, о чем нелегко догадаться и что должно часто меняться. Лучше всего этим требованиям отвечает Kerberos. В Kerberos всякий раз, когда две сущности хотят обменяться информацией, центр сертификации выдает «мандат», содержащий временный сеансовый ключ. Этот сеансовый ключ используется как общий секрет. Когда я хочу отправить вам подпись, я получаю мандат на общение с вами. Я открываю свою часть мандата, чтобы получить S, и посылаю вам сообщение, его HMAC и вашу часть мандата. Вы открываете мандат (используя пароль, который вы сначала зарегистрировали в Kerberos) и получаете S и мои идентификационные данные. Теперь вы можете взять сообщение M, создать собственный H'(S+M) и увидеть, совпадают ли они. Если совпадают, вы знаете, что вы получили мое сообщение невредимым, а Kerberos сообщает вам, кто я такой.

    Другой метод защиты хэша — использовать криптографию с открытым ключом, такую как RSA. В криптографии с открытым ключом существует два ключа, секретный ключ, известный только его обладателю, и открытый ключ, доступный любому, кто хочет связаться с владельцем ключа. В криптографии с открытым ключом все, что было закодировано с помощью секретного ключа, может быть декодировано открытым ключом и наоборот.

    Давайте рассмотрим простой пример, который демонстрирует криптографию с открытым ключом. В этом примере мы ограничим наше сообщение буквами от a до z и присвоим им значения от одного до 26. Чтобы закодировать, добавим значение секретного ключа; в данном случае — +4:

    Буква h e l l o
    Числовое значение 8 5 12 12 15
    Секретный ключ 4 4 4 4 4
    Закодированное значение 12 9 16 16 19

    Кодирование сообщения

    Чтобы декодировать, мы добавляем открытый ключ, который будет +22; если результат находится вне числового диапазона, мы добавляем или вычитает 26 до тех пор, пока не получим верного значения. (иначе говоря, чтобы декодировать, мы добавляем открытый ключ и находим остаток от деления на 26).

    Закодированное значение 12 9 16 16 19
    Открытый ключ 22 22 22 22 22
    Необработанное дешифрованное значение 34 31 38 38 41
    Нормализованное значение 8 5 12 12 15
    Незашифрованный текст h e l l o

    Декодирование сообщения

    RSA работает точно так же, за исключением того, что вместо сложения мы используем возведение в степень и числа на сотни символов длиннее.

    Используя RSA, я формирую хэш H(M) и кодирую его с помощью моего секретного ключа. Получаю {H(M)}private-key — это и есть подпись. Когда вы получаете сообщение M, вы создаете хэш H'(M) и декодируете подпись, используя мой открытый ключ. Получаете сгенерированный мною H(M). Если H(M) и H'(M) одинаковы, мы знаем, что M идентично. Более того, вы знаете, что кто бы ни обладал секретным ключом — т.е. я — он является отправителем сообщения.

    Формат подписи

    XML-DSIG использует отдельное пространство имен, и мы принимаем, что в наших примерах присутствует следующее объявление:

    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
    

    Самый верхний элемент <ds:Signature> довольно прост. В нем содержится информация о том, что было подписано, подпись, ключи, используемые для создания подписи, и место для сохранения произвольных данных:

        <element name="Signature" type="ds:SignatureType"/>
        <complexType name="SignatureType">
          <sequence> 
            <element ref="ds:SignedInfo"/> 
            <element ref="ds:SignatureValue"/> 
            <element ref="ds:KeyInfo" minOccurs="0"/> 
            <element ref="ds:Object" minOccurs="0" maxOccurs="unbounded"/> 
          </sequence>  
          <attribute name="Id" type="ID" use="optional"/>
        </complexType>
    

    Рассмотрим все элементы в порядке возрастания сложности.

         Id
         ds:SignatureValue
         ds:Object
         ds:SignedInfo
         ds:KeyInfo
    
    Атрибут ds:Signature/@Id

    Глобальный атрибут Id позволяет документу содержать множество подписей и обеспечивает возможность идентификации отдельных экземпляров. Использование множества подписей обычно применяется в деловых политиках, таких, когда и управляющий, и отдел перевозок должны подтвердить транспортные накладные.

    Элемент ds:SignatureValue

    Этот элемент содержит саму подпись. Поскольку всегда подписи — это двоичные данные, XML DSIG указывает, что значение подписи — это всегда простой элемент с Base64-кодированным содержимым:

        <element name="SignatureValue" type="ds:SignatureValueType"/> 
        <complexType name="SignatureValueType">
          <simpleContent>
            <extension base="base64Binary">
              <attribute name="Id" type="ID" use="optional"/>
            </extension>
          </simpleContent>
        </complexType>
    

    Чтобы интерпретировать SignatureValue, важно понимать содержимое элемента SignedInfo, который будет обсуждаться далее. До тех пор, это всего лишь непрозрачная строка байтов:

        <SignatureValue>
            WvZUJAJ/3QNqzQvwne2vvy7U5Pck8ZZ5UTa6pIwR7GE+PoGi6A1kyw==
        </SignatureValue>
    
    Элемент ds:Signature/ds:Object

    Как мы увидим ниже, XML DSIG может содержать множество элементов. Элемент всегда сможет существовать самостоятельно, как Web-страница или деловой XML-документ, но иногда элемент лучше интерпретировать как метаданные для подписанного «настоящего» содержимого. Например, данные могут быть «собственностью» подписи, как временная метка создания подписи.

    Для размещения таких данных в Signature может использоваться элемент ds:Object:

        <element name="Object" type="ds:ObjectType"/> 
        <complexType name="ObjectType" mixed="true">
          <sequence minOccurs="0" maxOccurs="unbounded">
            <any namespace="##any" processContents="lax"/>
          </sequence>
          <attribute name="Id" type="ID" use="optional"/> 
          <attribute name="MimeType" type="string" use="optional"/>
          <attribute name="Encoding" type="anyURI" use="optional"/> 
        </complexType>
    

    Атрибут Id позволяет содержать в подписи множество объектов, к каждому из которых можно обращаться независимо. MimeType используется для идентификации данных, чтобы другие обработчики могли их использовать; он не имеет значения для DSIG-обработчика.

    Encoding определяет, как подготовить содержимое к обработке; на данное время определено только кодирование base-64.

    Вот два объекта (с идентичным содержимым), которые могут использоваться как простые указатели того, когда был подписан документ. Сервис, содержащий их в своей подписи, может быть полезен для конкурсов, аукционов или других проводимых в режиме онлайн действий, которые имеют предельные сроки представления:

        <ds:Object Id="ts-bin" Encoding="http://www.w3.org/2000/09/xmldsig#base64">
            V2VkIEp1biAgNCAxMjoxMTowMyBFRFQgMjAwMwo
        </ds:Object>
        <ds:Object Id="ts-text">
            Wed Jun  4 12:11:06 EDT
        </ds:Object>
    
    Элемент ds:SignedInfo

    Вы когда-нибудь слышали афоризм: «Любая проблема в вычислительной техники может быть решена на другом уровне абстракции»? Итак, как мы сейчас убедимся, XML DSIG – лучший пример этому.

    Содержимое ds:SignedInfo может быть разделено на две части: информация о SignatureValue и информация о содержимом приложения, — как видно из следующего фрагмента XML Schema:

       <element name="SignedInfo" type="ds:SignedInfoType"/> 
       <complexType name="SignedInfoType">
         <sequence> 
           <element ref="ds:CanonicalizationMethod"/>
           <element ref="ds:SignatureMethod"/> 
           <element ref="ds:Reference" maxOccurs="unbounded"/> 
         </sequence>  
         <attribute name="Id" type="ID" use="optional"/> 
       </complexType>
    

    Синтаксис XML довольно небрежен. Например, порядок атрибутов и то, как оформляются значения, на самом деле не имеет особого значения. Поскольку речь идет о программном обеспечении для обработки XML, следующие два примера полностью равнозначны:

        <a foo='yes' boo="no"/>
        <a boo="no" foo="yes"  ></a>
    

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

    Чтобы это обработать, содержимое должно быть канонизировано (canonicalized). Канонизация или C14N — это процесс выбора одного пути из всех возможных вариантов выхода, так чтобы отправитель и получатель могли формировать именно такое же байтовое значение. То, какое промежуточное программное обеспечение XML может быть вовлечено в это, значения не имеет. C14N — это серьезный предмет, заслуживающий отдельной статьи.

    Элемент ds:SignedInfo/ds:CanonicalizationMethod определяет то, как точно воспроизводить поток байтов. Элемент ds:SignedInfo/ds:SignatureMethod определяет, какой тип подписи — например, Kerberos или RSA — используется для создания подписи. Вместе эти два элемента указывают нам, как создать хэш и как защитить его от изменений.

    Вот пример:

        <ds:SignedInfo>
            <ds:CanonicalizationMethod
                 Algorithm="http://www.w3.org/2001/10/xml-exc-c14n"/>
            <ds:SignatureMethod
                 Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
            ...
    
    Элемент ds:Reference

    Элемент ds:SignatureValue содержит подпись, которая охватывает только элемент ds:SignedInfo: в хэш подписи включено только содержимое ds:SignedInfo. Тогда как мы на самом деле подписываем все остальное содержимое? Тонкость и мощь — в элементе ds:Reference.

    Из описания схемы элемента ds:SignedInfoType, приведенного выше, подпись может иметь множество ссылок. Это позволяет одной XML DSIG охватывать множество объектов: все части MIME-сообщения, XML-файл и XSLT-сценарий, который преобразовывает его в HTML, и т.д.

    Элемент ds:Reference ссылается на другое содержимое. Он включает хэш содержимого, свидетельство того, что хэш был создан (например, SHA1), и определение того, как содержимое должно бы трансформировано перед генерированием хэша. Трансформации обеспечивают поразительную гибкость XML DSIG. Здесь представлен фрагмент Схемы:

       <element name="Reference" type="ds:ReferenceType"/>
       <complexType name="ReferenceType">
         <sequence> 
           <element ref="ds:Transforms" minOccurs="0"/> 
           <element ref="ds:DigestMethod"/> 
           <element ref="ds:DigestValue"/> 
         </sequence>
         <attribute name="Id" type="ID" use="optional"/> 
         <attribute name="URI" type="anyURI" use="optional"/> 
         <attribute name="Type" type="anyURI" use="optional"/> 
       </complexType>
    

    Атрибут Type может обеспечить подсказку при обработке, но, в общем, бесполезен.

    URI указывает на фактическое содержимое, к которому обращаются. Поскольку это — URI, нам доступна вся мощь Web. Например, я могут подписать содержимое начальной страницы MSDN:

        <ds:Reference URI="http://msdn.microsoft.com">
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>HB7i8RaV7ZvuUlaTzZVx0S3POpU=</ds:DigestValue>
        </ds:Reference>
    

    Я также могу сослаться на содержимое XML-документа, такое как показанная выше временная метка:

        <ds:Reference URI="#ts-text">
            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <ds:DigestValue>pN3j2OeC0+/kCatpvy1dYfG1g68=</ds:DigestValue>
        </ds:Reference>
    

    И, конечно же, я могу поместить обе эти ссылки в одну и ту же подпись.

    Для подписания SOAP-сообщения с WS-Security чаще всего используется фрагмент URI:

        <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
            <SOAP:Header>
               <wsse:Security>
                       xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
                   ...
                   <ds:Signature>
                        ...
                        <ds:SignedInfo>
                            <ds:Reference URI='#Body'>
                                ...
                            </ds:Reference>
                            ...
                        <ds:SignedInfo>
                        ...
                   </ds:Signature>
                   ...
               </wsse:Security>
            </SOAP:Header>
            <SOAP:Body Id='Body'>
                ...
            </SOAP:Body>
        </SOAP:Envelope>
    

    Как вы, вероятно, и ожидали, ds:DigestMethod определяет алгоритм хэширования, а ds:DigestValue — Base64-значение хэша содержимого.

    Самая значительная часть элемента ds:Reference — набор преобразований, которые могут появиться. ds:Transforms — это просто список элементов ds:Transform, каждый из которых определяет шаг в процессе обработки. Схема определяет массив преобразований, одна из которых — ds:XPath — имеет определенную структуру:

       <element name="Transforms" type="ds:TransformsType"/>
       <complexType name="TransformsType">
         <sequence>
           <element ref="ds:Transform" maxOccurs="unbounded"/>  
         </sequence>
       </complexType>
    
       <element name="Transform" type="ds:TransformType"/>
       <complexType name="TransformType" mixed="true">
         <choice minOccurs="0" maxOccurs="unbounded"> 
           <any namespace="##other" processContents="lax"/>
           <element name="XPath" type="string"/> 
         </choice>
         <attribute name="Algorithm" type="anyURI" use="required"/> 
       </complexType>
    

    Содержимое результата преобразования будет зависеть от атрибута Algorithm. Например, если простой XML был подписан, тогда, вероятнее всего, будет единственное преобразование, определяющее алгоритм C14N:

      <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n"/>
      </ds:Transforms>
    

    XML DSIG определяет несколько преобразований, включая преобразование XPath, которое облегчает подписание части документа, например, подписание только разметки при игнорировании всего текста:

        <ds:Transforms>
            <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
                <XPath>not(self::text())</XPath>
            </ds:Transform>
            <ds:Transform
                    Algorithm="http://www.w3.org/TR/2001/10/xml-exc-c14n/>
            </ds:Transforms>
    

    Другие оговоренные преобразования включают встроенные таблицы стилей XSLT, декодирование закодированных данных и т.д.

    Элемент ds:KeyInfo

    На данный момент мы знаем, как сослаться на содержимое, преобразовать и хэшировать его, и создать подпись, которая покрывает (защищает) это содержимое. Оно защищено от обратного преобразования: ds:SignatureValue включает ds:SignedInfo, который содержит ds:References, в котором находятся значения хэшей данных приложения. Изменение любого из этих элементов влечет за собой разрушение цепочки математических вычислений, и подпись не будет достоверной.

    Единственное, что осталось сделать — идентифицировать подписавшую сторону или, по крайней мере, ключ, который сгенерировал подпись (или, говоря на языке криптографии, ключ, который защищает хэш от изменений). Это делает элемент ds:KeyInfo:

       <element name="KeyInfo" type="ds:KeyInfoType"/> 
       <complexType name="KeyInfoType" mixed="true">
         <choice maxOccurs="unbounded">     
           <element ref="ds:KeyName"/> 
           <element ref="ds:KeyValue"/> 
           <element ref="ds:RetrievalMethod"/> 
           <element ref="ds:X509Data"/> 
           <element ref="ds:PGPData"/> 
           <element ref="ds:SPKIData"/>
           <element ref="ds:MgmtData"/>
           <any processContents="lax" namespace="##other"/>
           <!-- (1,1) elements from (0,unbounded) namespaces -->
         </choice>
         <attribute name="Id" type="ID" use="optional"/>
       </complexType>
    

    Как видите, XML DSIG поддерживает разнообразные типы ключей и инфраструктуры ключей, а WS-Security пошла еще дальше. Мы рассмотрим только простое имя и сертификат X.509. ds:KeyName стоит использовать при создании специального приложения для закрытой среды:

        <element name="KeyName" type="string"/>
    

    За проецирование имени во внутреннее хранилище и выбор подходящего ключа отвечает процесс, проверяющий достоверность подписи. К обычным значениям ds:KeyName относятся адрес электронной почты или элемент справочника.

    Сертификаты X.509 поддерживаются элементом ds:X509Data. Этот элемент позволяет подписывающей стороне встраивать свой сертификат (в Base64) или любую из нескольких альтернативных форм идентификации сертификата: имя субъекта, имя пользователя и серийный номер, идентификатор ключа или что-либо другое. Подписывающая сторона также может включить текущую копию Списка отзыва сертификата (Certificate Revocation List - CRL), чтобы показать, что идентификатор подписывающей стороны был действителен в момент подписания документа. Фрагмент Схемы, приведенный ниже, показывает различные способы идентификации сертификата X.509:

        <element name="X509Data" type="ds:X509DataType"/> 
        <complexType name="X509DataType">
          <sequence maxOccurs="unbounded">
            <choice>
              <element name="X509IssuerSerial" type="ds:X509IssuerSerialType"/>
              <element name="X509SKI" type="base64Binary"/>
              <element name="X509SubjectName" type="string"/>
              <element name="X509Certificate" type="base64Binary"/>
              <element name="X509CRL" type="base64Binary"/>
              <any namespace="##other" processContents="lax"/>
            </choice>
          </sequence>
        </complexType>
    
        <complexType name="X509IssuerSerialType"> 
          <sequence> 
            <element name="X509IssuerName" type="string"/> 
            <element name="X509SerialNumber" type="integer"/> 
          </sequence>
        </complexType>
    

    Поскольку различные приложения будут сохранять и извлекать сертификаты, используя различные схемы, цифровые подписи XML часто включают множество имен одного и того же ключа, встраивая их в один и тот же элемент ds:KeyInfo. В этом примере мы привели и удобное для пользователя имя (полезно для GUI-приложения) и уникальный идентификатор в форме издателя и серийного номера (полезно для поиска директории):

        <ds:KeyInfo>
            <ds:KeyName>
                rsalz@datapower.com
            </ds:KeyName>
            <ds:X509Data>
                <ds:X509SubjectName>
                    cn=Rich Salz, o=DataPower, c=US
                </ds:X509SubjectName>
                <ds:X509IssuerSerial>
                    <ds:IssuerName>
                        ou=Development, o=DataPower, c=US
                    </ds:IssuerName>
                    <ds:SerialNumber>32</ds:SerialNumber>
                </ds:X509IssuerSerial>
            </ds:X509Data>
        </ds:KeyInfo>
    
    Заключение

    Я провел развернутый обзор спецификации XML DSIG, используя определение схемы для описания доступных возможностей и обработку, необходимую для генерирования и проверки достоверности документа XML DSIG. Мы начали с основного элемента подписи (ds:SignedInfo), рассмотрели, как он объединяет ссылки на содержимое приложения, чтобы защитить его, и закончили рассмотрением элемента ds:KeyInfo, чтобы увидеть, как приложение может проверить достоверность подписи и, возможно, подтвердить идентичность создавшего подпись. Эти три аспекта обеспечивают самые основные и низкоуровневые компоненты защиты целостности содержимого XML (и другого). Неудивительно, их гибкость означает, что прямое их использование может быть слишком сложным.

    Одно из самых главных применений XML DSIG конечно же ожидается со спецификаций WS-Security. Она обеспечивает более ориентированный на приложение подход к защите данных и аутентификации пользователя. Чтобы узнать, как XML DSIG используется в WS-Security, смотрите Понимание WS-Security (Understanding WS-Security) (http://www.uneta.org/Default.aspx?mnuid=E5ED8936-5C6D-4DFD-8A3A-7CDAD3923183&artID=20943495-6964-49C6-9F20-184C14E4E759).

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

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

       
       
         
      VBNet рекомендует