Subject: Re: [GOST] Формат подписи CMS в pygost

From: Sergey Matveev <[email-masked]>
Date: Thu, 30 Nov 2017 23:05:53 +0300

Приветствую!

*** Гневашев Дмитрий [2017-11-30 19:11]:
>У меня проблема в том, что принимающая сторона отклонила запрос на сертификат
>с такой формулировкой:
>
>"чтобы получить CMS формат, нужно использовать функцию CryptSignMessage  или
>функцию CryptSignHash , но инвертировать байты самостоятельно"

Тут видимо они намекают на какой-то продукт/решение/программу -- тут уж
ничего не могу сказать.

>Есть ли в pygost возможность подписывать документы в формате CMS?

Вы делаете документ CMS (видимо, SignedData) и вопрос может ли pygost
сделать подпись которая окажется внутри SignerInfo этого CMS? Если так,
то он из коробки делает подпись которая без проблем засовывается в
SignerInfo как есть. Возможно только одно но: хэш, скорее всего, нужно
будет перевернуть перед подписью. Например:

    hasher = GOST34112012(digest_size=32)
    hasher.update(b'data to be signed')
    curve = gost3410.GOST3410Curve(*gost3410.CURVE_PARAMS['GostR3410_2012_TC26_ParamSetA'])
    prv_key = gost3410.prv_unmarshal(urandom(32))
    signature = gost3410.sign(curve, prv_key, hasher.digest()[::-1], mode=2012)

Если же речь вообще про создание CMS, то pygost этого не умеет и вряд ли
будет, так как это уже не касается криптографических алгоритмов как
таковых, а форматов представления данных. CMS это RFC 5652 на 56 страниц
(плюс ещё ряд RFC касающихся использования ГОСТ алгоритмов в контексте
CMS) -- формат для описания того как подписывать, шифровать,
аутентифицировать и проверять целостность документов, используя ASN.1
DER кодек.

Примерно, в общих чертах, CMS с подписанными данными делается как-то так
ниже. Наверняка он не валиден (я версию выставил наобум) и что-то сделал
не так, но общий смысл создания примерно описан ниже. Использую
библиотеку PyDERASN (http://pyderasn.cypherpunks.ru/) для работы с ASN.1.
Идентификатор публичного ключа подписанта я делаю urandom(20) -- а
должен браться из расширения X.509 сертификата.

------------------------ >8 ------------------------
from base64 import b64encode
from os import urandom

from pyderasn import Any
from pyderasn import Choice
from pyderasn import Integer
from pyderasn import Null
from pyderasn import ObjectIdentifier
from pyderasn import OctetString
from pyderasn import Sequence
from pyderasn import SetOf
from pyderasn import tag_ctxc
from pyderasn import tag_ctxp
from pygost import gost3410
from pygost.gost34112012 import GOST34112012

class KeyIdentifier(OctetString):
    pass

class SubjectKeyIdentifier(KeyIdentifier):
    pass

class SignerIdentifier(Choice):
    schema = (
        ('subjectKeyIdentifier', SubjectKeyIdentifier(impl=tag_ctxp(0))),
    )

class SignatureValue(OctetString):
    pass

class AttributeValue(Any):
    pass

class AttributeValues(SetOf):
    schema = AttributeValue()

class Attribute(Sequence):
    schema = (
        ('attrType', ObjectIdentifier()),
        ('attrValues', AttributeValues()),
    )

class SignedAttributes(SetOf):
    schema = Attribute()
    bounds = (1, 32)

class CMSVersion(Integer):
    schema = (
        ('v0', 0),
    )

class AlgorithmIdentifier(Sequence):
    schema = (
        ('algorithm', ObjectIdentifier()),
        ('parameters', Any(optional=True)),
    )

class SignatureAlgorithmIdentifier(AlgorithmIdentifier):
    pass

class DigestAlgorithmIdentifier(AlgorithmIdentifier):
    pass

class SignerInfo(Sequence):
    schema = (
        ('version', CMSVersion()),
        ('sid', SignerIdentifier()),
        ('digestAlgorithm', DigestAlgorithmIdentifier()),
        ('signedAttrs', SignedAttributes(impl=tag_ctxc(0), optional=True)),
        ('signatureAlgorithm', SignatureAlgorithmIdentifier()),
        ('signature', SignatureValue()),
    )

class SignerInfos(SetOf):
    schema = SignerInfo()

class DigestAlgorithmIdentifiers(SetOf):
    schema = AlgorithmIdentifier()

class ContentType(ObjectIdentifier):
    pass

class EncapsulatedContentInfo(Sequence):
    schema = (
        ('eContentType', ContentType()),
        ('eContent', OctetString(expl=tag_ctxc(0), optional=True)),
    )

class SignedData(Sequence):
    schema = (
        ('version', CMSVersion()),
        ('digestAlgorithms', DigestAlgorithmIdentifiers()),
        ('encapContentInfo', EncapsulatedContentInfo()),
        ('signerInfos', SignerInfos()),
    )

class ContentInfo(Sequence):
    schema = (
        ('contentType', ContentType()),
        ('content', Any(expl=tag_ctxc(0))),
    )

id_contentType = ObjectIdentifier('1.2.840.113549.1.9.3')
id_messageDigest = ObjectIdentifier('1.2.840.113549.1.9.4')
id_data = ObjectIdentifier('1.2.840.113549.1.7.1')
id_signedData = ObjectIdentifier('1.2.840.113549.1.7.2')
id_tc26_signwithdigest_gost3410_2012_256 = ObjectIdentifier('1.2.643.7.1.1.3.2')
id_tc26_gost3411_2012_256 = ObjectIdentifier('1.2.643.7.1.1.2.2')

ai_tc26_signwithdigest_gost3410_2012_256 = AlgorithmIdentifier()
ai_tc26_signwithdigest_gost3410_2012_256['algorithm'] = id_tc26_signwithdigest_gost3410_2012_256
ai_tc26_gost3411_2012_256 = AlgorithmIdentifier()
ai_tc26_gost3411_2012_256['algorithm'] = id_tc26_gost3411_2012_256
ai_tc26_gost3411_2012_256['parameters'] = Any(Null())

eci = EncapsulatedContentInfo()
eci['eContentType'] = ContentType(id_data)
eci['eContent'] = OctetString(b'data to be signed')

hasher = GOST34112012(digest_size=32)
hasher.update(bytes(eci['eContent']))
signed_attrs = SignedAttributes()
for t, v in (
    (id_contentType, eci['eContentType']),
    (id_messageDigest, OctetString(hasher.digest())),
):
    signed_attr = Attribute()
    signed_attr['attrType'] = t
    signed_attr['attrValues'] = AttributeValues((AttributeValue(v),))
prv_key = gost3410.prv_unmarshal(urandom(32))
curve = gost3410.GOST3410Curve(
    *gost3410.CURVE_PARAMS['GostR3410_2001_CryptoPro_A_ParamSet']
)
hasher = GOST34112012(digest_size=32)
hasher.update(signed_attrs.encode())
signature = gost3410.sign(curve, prv_key, hasher.digest()[::-1], mode=2001)

si = SignerInfo()
si['version'] = CMSVersion('v0')
si['sid'] = SignerIdentifier(('subjectKeyIdentifier', SubjectKeyIdentifier(urandom(20))))
si['digestAlgorithm'] = DigestAlgorithmIdentifier(ai_tc26_gost3411_2012_256)
si['signatureAlgorithm'] = SignatureAlgorithmIdentifier(
    ai_tc26_signwithdigest_gost3410_2012_256
)
si['signature'] = SignatureValue(signature)

sd = SignedData()
sd['version'] = CMSVersion('v0')
sd['digestAlgorithms'] = DigestAlgorithmIdentifiers((ai_tc26_gost3411_2012_256,))
sd['encapContentInfo'] = eci
sd['signerInfos'] = SignerInfos((si,))
ci = ContentInfo()
ci['contentType'] = ContentType(id_signedData)
ci['content'] = Any(sd)
print(b64encode(ci.encode()))
------------------------ >8 ------------------------

В итоге получится например вот такой вот CMS:

MIG/BgkqhkiG9w0BBwKggbEwga4CAQAxDjAMBggqhQMHAQECAgUAMCAGCSqGSIb3DQEHAaAT
BBFkYXRhIHRvIGJlIHNpZ25lZDF3MHUCAQCAFGXAA/03TMbk3IX+JpPXm7BchKefMAwGCCqF
AwcBAQICBQAwCgYIKoUDBwEBAwIEQO+QDSE4Ayt0YzGnTw178X0ruXlM6FDWGq2tJXpWwGnB
TAYTi4AGwbIMfCzeanbfMWwv6lvTDc7XTUGZ/UjcxYY=

внутренности которого выглядят как-то так:
------------------------ >8 ------------------------
  0   [1,2, 191] ContentInfo SEQUENCE
  3   [1,1,   9]  . contentType: ContentType OBJECT IDENTIFIER id_signedData (1.2.840.113549.1.7.2)
 17-3 [0,0, 177]  . content: [0] EXPLICIT [UNIV 16] ANY
                  . . 30:81:AE:02:01:00:31:0E:30:0C:06:08:2A:85:03:07
                  . . 01:01:02:02:05:00:30:20:06:09:2A:86:48:86:F7:0D
                  . . 01:07:01:A0:13:04:11:64:61:74:61:20:74:6F:20:62
                  . . 65:20:73:69:67:6E:65:64:31:77:30:75:02:01:00:80
                  . . 14:65:C0:03:FD:37:4C:C6:E4:DC:85:FE:26:93:D7:9B
                  . . B0:5C:84:A7:9F:30:0C:06:08:2A:85:03:07:01:01:02
                  . . 02:05:00:30:0A:06:08:2A:85:03:07:01:01:03:02:04
                  . . 40:EF:90:0D:21:38:03:2B:74:63:31:A7:4F:0D:7B:F1
                  . . 7D:2B:B9:79:4C:E8:50:D6:1A:AD:AD:25:7A:56:C0:69
                  . . C1:4C:06:13:8B:80:06:C1:B2:0C:7C:2C:DE:6A:76:DF
                  . . 31:6C:2F:EA:5B:D3:0D:CE:D7:4D:41:99:FD:48:DC:C5
                  . . 86
 14   [1,2, 174]  . . DEFINED BY (1.2.840.113549.1.7.2): SignedData SEQUENCE
 17   [1,1,   1]  . . . version: CMSVersion INTEGER v0
 20   [1,1,  14]  . . . digestAlgorithms: DigestAlgorithmIdentifiers SET OF
 22   [1,1,  12]  . . . . 0: AlgorithmIdentifier SEQUENCE
 24   [1,1,   8]  . . . . . algorithm: OBJECT IDENTIFIER id_tc26_gost3411_2012_256 (1.2.643.7.1.1.2.2)
 34   [0,0,   2]  . . . . . parameters: [UNIV 5] ANY OPTIONAL
                  . . . . . . 05:00
 36   [1,1,  32]  . . . encapContentInfo: EncapsulatedContentInfo SEQUENCE
 38   [1,1,   9]  . . . . eContentType: ContentType OBJECT IDENTIFIER id_data (1.2.840.113549.1.7.1)
 51-2 [1,1,  17]  . . . . eContent: [0] EXPLICIT OCTET STRING 17 bytes OPTIONAL
                  . . . . . 64:61:74:61:20:74:6F:20:62:65:20:73:69:67:6E:65
                  . . . . . 64
 70   [1,1, 119]  . . . signerInfos: SignerInfos SET OF
 72   [1,1, 117]  . . . . 0: SignerInfo SEQUENCE
 74   [1,1,   1]  . . . . . version: CMSVersion INTEGER v0
 77   [0,0,  22]  . . . . . sid: SignerIdentifier CHOICE subjectKeyIdentifier
 77   [1,1,  20]  . . . . . . subjectKeyIdentifier: [0] SubjectKeyIdentifier OCTET STRING 20 bytes
                  . . . . . . . 65:C0:03:FD:37:4C:C6:E4:DC:85:FE:26:93:D7:9B:B0
                  . . . . . . . 5C:84:A7:9F
 99   [1,1,  12]  . . . . . digestAlgorithm: DigestAlgorithmIdentifier SEQUENCE
101   [1,1,   8]  . . . . . . algorithm: OBJECT IDENTIFIER id_tc26_gost3411_2012_256 (1.2.643.7.1.1.2.2)
111   [0,0,   2]  . . . . . . parameters: [UNIV 5] ANY OPTIONAL
                  . . . . . . . 05:00
113   [1,1,  10]  . . . . . signatureAlgorithm: SignatureAlgorithmIdentifier SEQUENCE
115   [1,1,   8]  . . . . . . algorithm: OBJECT IDENTIFIER id_tc26_signwithdigest_gost3410_2012_256 (1.2.643.7.1.1.3.2)
125   [1,1,  64]  . . . . . signature: SignatureValue OCTET STRING 64 bytes
                  . . . . . . EF:90:0D:21:38:03:2B:74:63:31:A7:4F:0D:7B:F1:7D
                  . . . . . . 2B:B9:79:4C:E8:50:D6:1A:AD:AD:25:7A:56:C0:69:C1
                  . . . . . . 4C:06:13:8B:80:06:C1:B2:0C:7C:2C:DE:6A:76:DF:31
                  . . . . . . 6C:2F:EA:5B:D3:0D:CE:D7:4D:41:99:FD:48:DC:C5:86
------------------------ >8 ------------------------

Это всего-лишь подписанная 34.10-2012 256-бит с 34.11-2012 256-бит
строчка "data to be signed". CMS это очень не тривиально, плюс оно
разных версий бывает (CMSVersion).

>Какой смысл в функции prv_unmarshal? Она инвертирует байты ключа? Для чего
>она нужна?

Если честно, то уже смутно помню, но вроде бы сделана потому что видел
что приватные 34.10 ключи хранят в виде little-endian значений и эта
функция преобразует набор байт в little-endian число (bytes2long это
big-endian -- поэтому предварительно делается кручение).

-- 
Sergey Matveev (http://www.stargrave.org/)
OpenPGP: CF60 E89A 5923 1E76 E263  6422 AE1A 8109 E498 57EF