uakep 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uakep/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ """uakep: Ukrainian qualified electronic signatures (КЕП, ДСТУ 4145-2002) in pure Python.
2
+
3
+ from uakep import load_key, sign, unwrap
4
+
5
+ key = load_key("Key-6.dat", password="…", certificate="signer.cer")
6
+ signed = sign(receipt_xml_cp1251, key, timestamp=post) # CAdES with a signature time-stamp
7
+ content = unwrap(signed) # checks the signature, returns the data
8
+
9
+ ``post(url, request) -> bytes`` is yours: it POSTs the time-stamp request to the TSA named in
10
+ the signer's certificate. Async callers use ``await sign_async(..., timestamp=async_post)``.
11
+ Offline documents are signed without ``timestamp``.
12
+ """
13
+
14
+ from importlib.metadata import PackageNotFoundError, version
15
+
16
+ from .asn1 import UnsupportedError
17
+ from .cades import SignatureError, SigningKey, Verified, sign, sign_async, verify
18
+ from .certificate import Certificate, CertificateError
19
+ from .keystore import KeyFileError, WrongPasswordError, load_key
20
+ from .tsp import TimeStamp, TimeStampError
21
+
22
+ try:
23
+ __version__ = version("uakep")
24
+ except PackageNotFoundError: # running from a source tree that is not installed
25
+ __version__ = "0+unknown"
26
+
27
+
28
+ def unwrap(signed: bytes, *, detached_content: bytes | None = None) -> bytes:
29
+ """Check the ДСТУ 4145 signature on CAdES signed data and return the signed data.
30
+
31
+ Raises SignatureError when the signature, or a signature time-stamp, does not verify.
32
+ Certificate chains and revocation are not checked.
33
+ """
34
+ return verify(signed, detached_content=detached_content).content
35
+
36
+
37
+ __all__ = [
38
+ "Certificate",
39
+ "CertificateError",
40
+ "KeyFileError",
41
+ "SignatureError",
42
+ "SigningKey",
43
+ "TimeStamp",
44
+ "TimeStampError",
45
+ "UnsupportedError",
46
+ "Verified",
47
+ "WrongPasswordError",
48
+ "__version__",
49
+ "load_key",
50
+ "sign",
51
+ "sign_async",
52
+ "unwrap",
53
+ "verify",
54
+ ]
uakep/asn1.py ADDED
@@ -0,0 +1,406 @@
1
+ """ASN.1 structures for ДСТУ 4145 keys, parameters and key containers (on asn1crypto).
2
+
3
+ Curve parameters follow ДСТУ 4145-2002, Appendix Б: a named curve OID or an explicit binary
4
+ curve, optionally with a substitution table (ДКЕ). In certificates and private keys the
5
+ coefficient ``b`` and the compressed base point are little-endian octet strings; inside IIT's
6
+ key attributes they are big-endian.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from asn1crypto import core, tsp, x509
12
+
13
+ from .curve import NAMED_CURVES, Curve
14
+
15
+ # Algorithms
16
+ DSTU4145_LE = "1.2.804.2.1.1.1.1.3.1.1"
17
+ DSTU4145_KUPYNA = "1.2.804.2.1.1.1.1.3.6.1.1"
18
+ GOST34311 = "1.2.804.2.1.1.1.1.2.1"
19
+ GOST34311_HMAC = "1.2.804.2.1.1.1.1.1.2"
20
+ GOST28147_CFB = "1.2.804.2.1.1.1.1.1.1.3"
21
+ PBES2 = "1.2.840.113549.1.5.13"
22
+ PBKDF2 = "1.2.840.113549.1.5.12"
23
+
24
+ # IIT key containers (Key-6.dat) and the attributes carrying a second key
25
+ IIT_STORE = "1.3.6.1.4.1.19398.1.1.1.2"
26
+ IIT_KEY_CURVE = "1.3.6.1.4.1.19398.1.1.2.2"
27
+ IIT_KEY_BITS = "1.3.6.1.4.1.19398.1.1.2.3"
28
+
29
+ # Named curves: the ten curves of ДСТУ 4145-2002 under the ГОСТ 34.311 and Купина arcs.
30
+ _CURVE_SIZES = (163, 167, 173, 179, 191, 233, 257, 307, 367, 431)
31
+ CURVE_OIDS: dict[str, str] = {}
32
+ for _arc in ("1.2.804.2.1.1.1.1.3.1.1.2", "1.2.804.2.1.1.1.1.3.6.1.1.2"):
33
+ for _i, _m in enumerate(_CURVE_SIZES):
34
+ CURVE_OIDS[f"{_arc}.{_i}"] = f"DSTU_PB_{_m}"
35
+
36
+
37
+ class UnsupportedError(ValueError):
38
+ """Valid input that this version of uakep cannot handle (an unbundled curve, a cipher)."""
39
+
40
+
41
+ class Pentanomial(core.Sequence):
42
+ _fields = [("k", core.Integer), ("j", core.Integer), ("l", core.Integer)]
43
+
44
+
45
+ class Polynomial(core.Choice):
46
+ _alternatives = [("trinomial", core.Integer), ("pentanomial", Pentanomial)]
47
+
48
+
49
+ class FieldSpec(core.Sequence):
50
+ _fields = [("m", core.Integer), ("poly", Polynomial)]
51
+
52
+
53
+ class ECBinary(core.Sequence):
54
+ _fields = [
55
+ ("version", core.Integer, {"explicit": 0, "default": 0}),
56
+ ("field", FieldSpec),
57
+ ("a", core.Integer),
58
+ ("b", core.OctetString),
59
+ ("n", core.Integer),
60
+ ("bp", core.OctetString),
61
+ ]
62
+
63
+
64
+ class CurveSpec(core.Choice):
65
+ _alternatives = [("explicit", ECBinary), ("named", core.ObjectIdentifier)]
66
+
67
+
68
+ class DstuParams(core.Sequence):
69
+ """Algorithm parameters of a ДСТУ 4145 key: the curve and the optional ДКЕ."""
70
+
71
+ _fields = [
72
+ ("curve", CurveSpec),
73
+ ("dke", core.OctetString, {"optional": True}),
74
+ ("dke2", core.OctetString, {"optional": True}),
75
+ ]
76
+
77
+
78
+ class DstuAlgorithm(core.Sequence):
79
+ _fields = [("algorithm", core.ObjectIdentifier), ("parameters", DstuParams, {"optional": True})]
80
+
81
+
82
+ class KeyAttribute(core.Sequence):
83
+ _fields = [("type", core.ObjectIdentifier), ("values", core.SetOf, {"spec": core.Any})]
84
+
85
+
86
+ class KeyAttributes(core.SetOf):
87
+ _child_spec = KeyAttribute
88
+
89
+
90
+ class DstuPrivateKey(core.Sequence):
91
+ """PKCS#8 PrivateKeyInfo as ДСТУ 4145 key containers hold it; d is little-endian."""
92
+
93
+ _fields = [
94
+ ("version", core.Integer),
95
+ ("algorithm", DstuAlgorithm),
96
+ ("private_key", core.OctetString),
97
+ ("attributes", KeyAttributes, {"implicit": 0, "optional": True}),
98
+ ]
99
+
100
+
101
+ class IitStoreParams(core.Sequence):
102
+ _fields = [("mac", core.OctetString), ("pad", core.OctetString, {"optional": True})]
103
+
104
+
105
+ class IitStoreHeader(core.Sequence):
106
+ _fields = [("type", core.ObjectIdentifier), ("params", IitStoreParams)]
107
+
108
+
109
+ class IitStore(core.Sequence):
110
+ """IIT Key-6.dat: a private key encrypted with ГОСТ 28147-89 in ECB mode."""
111
+
112
+ _fields = [("header", IitStoreHeader), ("data", core.OctetString)]
113
+
114
+
115
+ class AlgorithmIdentifier(core.Sequence):
116
+ _fields = [("algorithm", core.ObjectIdentifier), ("parameters", core.Any, {"optional": True})]
117
+
118
+
119
+ class SubjectPublicKeyInfo(core.Sequence):
120
+ """X.509 public key info with the parameters left raw (asn1crypto rejects ДСТУ keys)."""
121
+
122
+ _fields = [("algorithm", AlgorithmIdentifier), ("public_key", core.BitString)]
123
+
124
+
125
+ class TbsCertificate(core.Sequence):
126
+ _fields = [
127
+ ("version", x509.Version, {"explicit": 0, "default": "v1"}),
128
+ ("serial_number", core.Integer),
129
+ ("signature", AlgorithmIdentifier),
130
+ ("issuer", x509.Name),
131
+ ("validity", x509.Validity),
132
+ ("subject", x509.Name),
133
+ ("subject_public_key_info", SubjectPublicKeyInfo),
134
+ ("issuer_unique_id", core.BitString, {"implicit": 1, "optional": True}),
135
+ ("subject_unique_id", core.BitString, {"implicit": 2, "optional": True}),
136
+ ("extensions", x509.Extensions, {"explicit": 3, "optional": True}),
137
+ ]
138
+
139
+
140
+ class Certificate(core.Sequence):
141
+ _fields = [
142
+ ("tbs_certificate", TbsCertificate),
143
+ ("signature_algorithm", AlgorithmIdentifier),
144
+ ("signature_value", core.BitString),
145
+ ]
146
+
147
+
148
+ class DirectoryAttribute(core.Sequence):
149
+ _fields = [("type", core.ObjectIdentifier), ("values", core.SetOf, {"spec": core.Any})]
150
+
151
+
152
+ class DirectoryAttributes(core.SequenceOf):
153
+ """subjectDirectoryAttributes, where Ukrainian CAs put РНОКПП and ЄДРПОУ."""
154
+
155
+ _child_spec = DirectoryAttribute
156
+
157
+
158
+ class AccessDescription(core.Sequence):
159
+ _fields = [("access_method", core.ObjectIdentifier), ("access_location", x509.GeneralName)]
160
+
161
+
162
+ class InfoAccess(core.SequenceOf):
163
+ """authorityInfoAccess and subjectInfoAccess."""
164
+
165
+ _child_spec = AccessDescription
166
+
167
+
168
+ # CMS (RFC 5652) as Ukrainian software writes it. The signer identifier is kept raw: the tax
169
+ # service's samples encode subjectKeyIdentifier as [0] EXPLICIT OCTET STRING, where RFC 5652
170
+ # has [0] IMPLICIT.
171
+
172
+ ID_DATA = "1.2.840.113549.1.7.1"
173
+ ID_SIGNED_DATA = "1.2.840.113549.1.7.2"
174
+ ID_TST_INFO = "1.2.840.113549.1.9.16.1.4"
175
+ ID_CONTENT_TYPE = "1.2.840.113549.1.9.3"
176
+ ID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4"
177
+ ID_SIGNING_TIME = "1.2.840.113549.1.9.5"
178
+ ID_SIGNING_CERTIFICATE_V2 = "1.2.840.113549.1.9.16.2.47"
179
+ ID_CONTENT_TIMESTAMP = "1.2.840.113549.1.9.16.2.20"
180
+ ID_SIGNATURE_TIMESTAMP = "1.2.840.113549.1.9.16.2.14"
181
+
182
+
183
+ class Attribute(core.Sequence):
184
+ _fields = [("type", core.ObjectIdentifier), ("values", core.SetOf, {"spec": core.Any})]
185
+
186
+
187
+ class Attributes(core.SetOf):
188
+ _child_spec = Attribute
189
+
190
+
191
+ class IssuerAndSerialNumber(core.Sequence):
192
+ _fields = [("issuer", x509.Name), ("serial_number", core.Integer)]
193
+
194
+
195
+ class SignerIdExplicit(core.Choice):
196
+ """The signer id as the tax samples write it: [0] EXPLICIT OCTET STRING for a key id."""
197
+
198
+ _alternatives = [
199
+ ("issuer_and_serial_number", IssuerAndSerialNumber),
200
+ ("subject_key_identifier", core.OctetString, {"explicit": 0}),
201
+ ]
202
+
203
+
204
+ class SignerIdImplicit(core.Choice):
205
+ """The signer id as RFC 5652 defines it: [0] IMPLICIT for a key id."""
206
+
207
+ _alternatives = [
208
+ ("issuer_and_serial_number", IssuerAndSerialNumber),
209
+ ("subject_key_identifier", core.OctetString, {"implicit": 0}),
210
+ ]
211
+
212
+
213
+ class SignerInfo(core.Sequence):
214
+ _fields = [
215
+ ("version", core.Integer),
216
+ ("sid", core.Any),
217
+ ("digest_algorithm", AlgorithmIdentifier),
218
+ ("signed_attrs", Attributes, {"implicit": 0, "optional": True}),
219
+ ("signature_algorithm", AlgorithmIdentifier),
220
+ ("signature", core.OctetString),
221
+ ("unsigned_attrs", Attributes, {"implicit": 1, "optional": True}),
222
+ ]
223
+
224
+
225
+ class SignerInfos(core.SetOf):
226
+ _child_spec = SignerInfo
227
+
228
+
229
+ class DigestAlgorithms(core.SetOf):
230
+ _child_spec = AlgorithmIdentifier
231
+
232
+
233
+ class CertificateSet(core.SetOf):
234
+ _child_spec = core.Any
235
+
236
+
237
+ class EncapsulatedContentInfo(core.Sequence):
238
+ _fields = [
239
+ ("content_type", core.ObjectIdentifier),
240
+ ("content", core.OctetString, {"explicit": 0, "optional": True}),
241
+ ]
242
+
243
+
244
+ class SignedData(core.Sequence):
245
+ _fields = [
246
+ ("version", core.Integer),
247
+ ("digest_algorithms", DigestAlgorithms),
248
+ ("encap_content_info", EncapsulatedContentInfo),
249
+ ("certificates", CertificateSet, {"implicit": 0, "optional": True}),
250
+ ("crls", CertificateSet, {"implicit": 1, "optional": True}),
251
+ ("signer_infos", SignerInfos),
252
+ ]
253
+
254
+
255
+ class ContentInfo(core.Sequence):
256
+ _fields = [
257
+ ("content_type", core.ObjectIdentifier),
258
+ ("content", core.Any, {"explicit": 0, "optional": True}),
259
+ ]
260
+ _oid_pair = ("content_type", "content")
261
+ _oid_specs = {ID_SIGNED_DATA: SignedData}
262
+
263
+
264
+ # Password-protected keys: PKCS#8 EncryptedPrivateKeyInfo with PBES2 (RFC 8018), PKCS#12 (RFC 7292)
265
+
266
+ ID_ENCRYPTED_DATA = "1.2.840.113549.1.7.6"
267
+ KEY_BAG = "1.2.840.113549.1.12.10.1.1"
268
+ SHROUDED_KEY_BAG = "1.2.840.113549.1.12.10.1.2"
269
+ CERT_BAG = "1.2.840.113549.1.12.10.1.3"
270
+ SAFE_CONTENTS_BAG = "1.2.840.113549.1.12.10.1.6"
271
+ X509_CERTIFICATE = "1.2.840.113549.1.9.22.1"
272
+ JKS_KEY_PROTECTOR = "1.3.6.1.4.1.42.2.17.1.1"
273
+
274
+
275
+ class EncryptedPrivateKeyInfo(core.Sequence):
276
+ _fields = [("encryption_algorithm", AlgorithmIdentifier), ("encrypted_data", core.OctetString)]
277
+
278
+
279
+ class Pbes2Params(core.Sequence):
280
+ _fields = [
281
+ ("key_derivation_func", AlgorithmIdentifier),
282
+ ("encryption_scheme", AlgorithmIdentifier),
283
+ ]
284
+
285
+
286
+ class Pbkdf2Params(core.Sequence):
287
+ _fields = [
288
+ ("salt", core.OctetString),
289
+ ("iteration_count", core.Integer),
290
+ ("key_length", core.Integer, {"optional": True}),
291
+ ("prf", AlgorithmIdentifier, {"optional": True}),
292
+ ]
293
+
294
+
295
+ class Gost28147Params(core.Sequence):
296
+ _fields = [("iv", core.OctetString), ("dke", core.OctetString, {"optional": True})]
297
+
298
+
299
+ class SafeBag(core.Sequence):
300
+ _fields = [
301
+ ("bag_id", core.ObjectIdentifier),
302
+ ("bag_value", core.Any, {"explicit": 0}),
303
+ ("bag_attributes", Attributes, {"optional": True}),
304
+ ]
305
+
306
+
307
+ class SafeContents(core.SequenceOf):
308
+ _child_spec = SafeBag
309
+
310
+
311
+ class CertBag(core.Sequence):
312
+ _fields = [
313
+ ("cert_id", core.ObjectIdentifier),
314
+ ("cert_value", core.OctetString, {"explicit": 0}),
315
+ ]
316
+
317
+
318
+ class EncryptedContentInfo(core.Sequence):
319
+ _fields = [
320
+ ("content_type", core.ObjectIdentifier),
321
+ ("content_encryption_algorithm", AlgorithmIdentifier),
322
+ ("encrypted_content", core.OctetString, {"implicit": 0, "optional": True}),
323
+ ]
324
+
325
+
326
+ class EncryptedData(core.Sequence):
327
+ _fields = [("version", core.Integer), ("encrypted_content_info", EncryptedContentInfo)]
328
+
329
+
330
+ class AuthenticatedSafe(core.SequenceOf):
331
+ _child_spec = ContentInfo
332
+
333
+
334
+ class Pfx(core.Sequence):
335
+ _fields = [
336
+ ("version", core.Integer),
337
+ ("auth_safe", ContentInfo),
338
+ ("mac_data", core.Any, {"optional": True}),
339
+ ]
340
+
341
+
342
+ class GeneralNames(core.SequenceOf):
343
+ _child_spec = x509.GeneralName
344
+
345
+
346
+ class IssuerSerial(core.Sequence):
347
+ _fields = [("issuer", GeneralNames), ("serial_number", core.Integer)]
348
+
349
+
350
+ class ESSCertIDv2(core.Sequence):
351
+ """RFC 5035; the hash algorithm is always written, as SHA-256 is never used here."""
352
+
353
+ _fields = [
354
+ ("hash_algorithm", AlgorithmIdentifier, {"optional": True}),
355
+ ("cert_hash", core.OctetString),
356
+ ("issuer_serial", IssuerSerial, {"optional": True}),
357
+ ]
358
+
359
+
360
+ class ESSCertIDsV2(core.SequenceOf):
361
+ _child_spec = ESSCertIDv2
362
+
363
+
364
+ class SigningCertificateV2(core.Sequence):
365
+ _fields = [("certs", ESSCertIDsV2), ("policies", core.Any, {"optional": True})]
366
+
367
+
368
+ class TimeStampResp(core.Sequence):
369
+ """RFC 3161 TimeStampResp with the token kept raw: it is CMS signed data with ДСТУ keys."""
370
+
371
+ _fields = [
372
+ ("status", tsp.PKIStatusInfo),
373
+ ("time_stamp_token", core.Any, {"optional": True}),
374
+ ]
375
+
376
+
377
+ def curve_from_spec(spec: CurveSpec, *, little_endian: bool = True) -> Curve:
378
+ """The curve a parameter block names or spells out."""
379
+ if spec.name == "named":
380
+ oid = spec.chosen.dotted
381
+ name = CURVE_OIDS.get(oid)
382
+ if name is None:
383
+ raise UnsupportedError(f"unknown curve OID {oid}")
384
+ if name not in NAMED_CURVES:
385
+ raise UnsupportedError(f"curve {name} is not bundled with uakep")
386
+ return NAMED_CURVES[name]
387
+ ec = spec.chosen
388
+ poly = ec["field"]["poly"]
389
+ if poly.name == "trinomial":
390
+ ks: tuple[int, ...] = (poly.chosen.native,)
391
+ else:
392
+ p = poly.chosen
393
+ ks = (p["k"].native, p["j"].native, p["l"].native)
394
+ order = "little" if little_endian else "big"
395
+ curve = Curve.from_params(
396
+ ec["field"]["m"].native,
397
+ ks,
398
+ ec["a"].native,
399
+ int.from_bytes(ec["b"].native, order),
400
+ ec["n"].native,
401
+ int.from_bytes(ec["bp"].native, order),
402
+ )
403
+ for named in NAMED_CURVES.values():
404
+ if named == curve:
405
+ return named
406
+ return curve