Skip to content

Commit

Permalink
Switch to asn1crypto entirely
Browse files Browse the repository at this point in the history
This should at least fix #44, but may also provide a more reliable fix for #9 for instance.
  • Loading branch information
ralphje committed Jun 2, 2024
1 parent 5dfe227 commit 2feeae8
Show file tree
Hide file tree
Showing 28 changed files with 595 additions and 1,227 deletions.
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ Release notes
=============
This page contains the most significant changes in Signify between each release.

v0.7.0 (unreleased)
-------------------
* Remove dependency of ``pyasn1`` and ``pyasn1-modules`` entirely to provide more robust parsing. The replacement,
``asn1crypto``, was already a dependency of this project, so we are mostly slimming down.

v0.6.1 (2024-03-21)
-------------------
* Require at least version v4.6.0 for requirement ``typing_extensions`` to ensure compatibility.
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ ignore_missing_imports = true

[tool.ruff]
target-version = "py38"

[tool.ruff.lint]
select = ["E", "W", "C4", "I", "N", "UP", "S", "B", "PERF", "RUF", "TID", "PTH", "PIE"]
ignore = ["B904", "S101"]

[tool.ruff.pep8-naming]
extend-ignore-names = ["componentType", "namedValues"]
[tool.ruff.lint.per-file-ignores]
"signify/asn1/*" = ["RUF012"]
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
pyasn1>=0.4.0,!=0.5.0
certvalidator>=0.11
asn1crypto>=1.3,<2
oscrypto>=1.1,<2
pyasn1-modules>=0.2.8
mscerts
typing_extensions>=4.6.0
2 changes: 0 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@
description="Module to generate and verify PE signatures",
long_description=long_description,
install_requires=[
"pyasn1>=0.4.0,!=0.5.0",
"certvalidator>=0.11",
"asn1crypto>=1.3,<2",
"oscrypto>=1.1,<2",
"pyasn1-modules>=0.2.8",
"mscerts",
"typing_extensions>=4.6.0",
],
Expand Down
1 change: 0 additions & 1 deletion signify/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@
from typing_extensions import TypeAlias

HashFunction: TypeAlias = Callable[[], "hashlib._Hash"]
OidTuple: TypeAlias = Tuple[int, ...]
61 changes: 2 additions & 59 deletions signify/asn1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,5 @@
from __future__ import annotations

from typing import Any, TypeVar, overload
from . import ctl, spc

from pyasn1.type.base import Asn1Type

from . import ctl, oids, pkcs7, spc

__all__ = ["pkcs7", "spc", "oids", "ctl", "guarded_ber_decode", "guarded_der_decode"]


_T = TypeVar("_T", bound=Asn1Type)


@overload
def guarded_ber_decode(data: Any, asn1_spec: _T) -> _T: ...


@overload
def guarded_ber_decode(data: Any, asn1_spec: None = None) -> Asn1Type: ...


def guarded_ber_decode(data: Any, asn1_spec: _T | None = None) -> Asn1Type | _T:
from pyasn1.codec.ber import decoder as ber_decoder

from signify import _print_type
from signify.exceptions import ParseError

try:
result, rest = ber_decoder.decode(data, asn1Spec=asn1_spec)
except Exception as e:
raise ParseError(f"Error while parsing {_print_type(asn1_spec)} BER: {e}")
if rest:
raise ParseError(
"Extra information after parsing %s BER" % _print_type(asn1_spec)
)
return result


@overload
def guarded_der_decode(data: Any, asn1_spec: _T) -> _T: ...


@overload
def guarded_der_decode(data: Any, asn1_spec: None = None) -> Asn1Type: ...


def guarded_der_decode(data: Any, asn1_spec: _T | None = None) -> Asn1Type | _T:
from pyasn1.codec.der import decoder as der_decoder

from signify import _print_type
from signify.exceptions import ParseError

try:
result, rest = der_decoder.decode(data, asn1Spec=asn1_spec)
except Exception as e:
raise ParseError(f"Error while parsing {_print_type(asn1_spec)} DER: {e}")
if rest:
raise ParseError(
"Extra information after parsing %s DER" % _print_type(asn1_spec)
)
return result
__all__ = ["spc", "ctl"]
233 changes: 150 additions & 83 deletions signify/asn1/ctl.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,165 @@
from pyasn1.type import namedtype, namedval, tag, univ
from pyasn1_modules import rfc2315, rfc5280
from __future__ import annotations

import datetime
import struct
from typing import Any, cast

from asn1crypto.algos import DigestAlgorithm
from asn1crypto.cms import ContentInfo, ContentType, EncapsulatedContentInfo
from asn1crypto.core import (
Asn1Value,
BMPString,
Integer,
ObjectIdentifier,
OctetString,
Sequence,
SequenceOf,
SetOf,
)
from asn1crypto.util import utc_with_dst
from asn1crypto.x509 import Extensions, ExtKeyUsageSyntax, Time

# Based on http://download.microsoft.com/download/C/8/8/C8862966-5948-444D-87BD-07B976ADA28C/%5BMS-CAESO%5D.pdf


class CTLVersion(univ.Integer): # type: ignore[misc]
namedValues = namedval.NamedValues(("v1", 0))
class CTLVersion(Integer): # type: ignore[misc]
_map = {
0: "v1",
}


class SubjectUsage(rfc5280.ExtKeyUsageSyntax): # type: ignore[misc]
class SubjectUsage(ExtKeyUsageSyntax): # type: ignore[misc]
pass


class ListIdentifier(univ.OctetString): # type: ignore[misc]
class ListIdentifier(OctetString): # type: ignore[misc]
pass


class SubjectIdentifier(univ.OctetString): # type: ignore[misc]
class SubjectIdentifier(OctetString): # type: ignore[misc]
pass


class TrustedSubject(univ.Sequence): # type: ignore[misc]
componentType = namedtype.NamedTypes(
namedtype.NamedType("subjectIdentifier", SubjectIdentifier()),
namedtype.OptionalNamedType("subjectAttributes", rfc2315.Attributes()),
)


class TrustedSubjects(univ.SequenceOf): # type: ignore[misc]
componentType = TrustedSubject()


class CertificateTrustList(univ.Sequence): # type: ignore[misc]
componentType = namedtype.NamedTypes(
namedtype.DefaultedNamedType("version", CTLVersion("v1")),
namedtype.NamedType("subjectUsage", SubjectUsage()),
namedtype.OptionalNamedType("listIdentifier", ListIdentifier()),
namedtype.OptionalNamedType("sequenceNumber", univ.Integer()),
namedtype.NamedType("ctlThisUpdate", rfc5280.Time()),
namedtype.OptionalNamedType("ctlNextUpdate", rfc5280.Time()),
namedtype.NamedType("subjectAlgorithm", rfc5280.AlgorithmIdentifier()),
namedtype.OptionalNamedType("trustedSubjects", TrustedSubjects()),
namedtype.OptionalNamedType(
"ctlExtensions",
rfc5280.Extensions().subtype(
explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)
),
),
)


# The following are known attributes


class EnhkeyUsage(rfc5280.ExtKeyUsageSyntax): # type: ignore[misc]
pass


class FriendlyName(univ.OctetString): # type: ignore[misc]
pass


class KeyIdentifier(univ.OctetString): # type: ignore[misc]
pass


class SubjectNameMd5Hash(univ.OctetString): # type: ignore[misc]
pass


class RootProgramCertPolicies(univ.OctetString): # type: ignore[misc]
# TODO: not implemented
pass


class AuthRootSha256Hash(univ.OctetString): # type: ignore[misc]
pass


class DisallowedFiletime(univ.OctetString): # type: ignore[misc]
pass


class RootProgramChainPolicies(rfc5280.ExtKeyUsageSyntax): # type: ignore[misc]
pass


class DisallowedEnhkeyUsage(rfc5280.ExtKeyUsageSyntax): # type: ignore[misc]
pass


class NotBeforeFiletime(univ.OctetString): # type: ignore[misc]
pass


class NotBeforeEnhkeyUsage(rfc5280.ExtKeyUsageSyntax): # type: ignore[misc]
pass
class SubjectAttributeType(ObjectIdentifier): # type: ignore[misc]
_map = {
"1.3.6.1.4.1.311.10.11.9": "microsoft_ctl_enhkey_usage",
"1.3.6.1.4.1.311.10.11.11": "microsoft_ctl_friendly_name",
"1.3.6.1.4.1.311.10.11.20": "microsoft_ctl_key_identifier",
"1.3.6.1.4.1.311.10.11.29": "microsoft_ctl_subject_name_md5_hash",
"1.3.6.1.4.1.311.10.11.83": "microsoft_ctl_root_program_cert_policies",
"1.3.6.1.4.1.311.10.11.98": "microsoft_ctl_auth_root_sha256_hash",
"1.3.6.1.4.1.311.10.11.104": "microsoft_ctl_disallowed_filetime",
"1.3.6.1.4.1.311.10.11.105": "microsoft_ctl_root_program_chain_policies",
"1.3.6.1.4.1.311.10.11.122": "microsoft_ctl_disallowed_enhkey_usage",
"1.3.6.1.4.1.311.10.11.126": "microsoft_ctl_not_before_filetime",
"1.3.6.1.4.1.311.10.11.127": "microsoft_ctl_not_before_enhkey_usage",
}


class SetOfSpecificOctetString(SetOf): # type: ignore[misc]
_child_spec = OctetString
children: Any

def parse(
self, spec: type[Asn1Value] | None = None, spec_params: Any = None
) -> Any:
if not spec:
return self.children
if issubclass(spec, SequenceOf):
self.children = [spec.load(child.contents) for child in self]
else:
self.children = [spec(contents=child.contents) for child in self]
return self.children


class CTLString(BMPString): # type: ignore[misc]
_encoding = "utf-16-le"

def __unicode__(self) -> str:
return cast(str, super().__unicode__().rstrip("\0"))


class FileTime(OctetString): # type: ignore[misc]
_epoch = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)
_native: datetime.datetime | None

@property
def native(self) -> datetime.datetime | None:
if self.contents is None or not self.contents:
return None

if self._native is None:
value = struct.unpack("<Q", self.contents)[0]
self._native = self._epoch + datetime.timedelta(microseconds=value / 10)

return self._native

def set(self, value: Any) -> None:
if isinstance(value, datetime.datetime):
if not value.tzinfo:
raise ValueError("Must be timezone aware")
value = value.astimezone(utc_with_dst)
value = struct.pack("<Q", int((value - self._epoch).total_seconds() * 100))
OctetString.set(self, value)
self._native = None


class SubjectAttribute(Sequence): # type: ignore[misc]
_fields = [
("type", SubjectAttributeType),
("values", SetOfSpecificOctetString),
]
_oid_specs: dict[str, type[Asn1Value]] = {
"microsoft_ctl_enhkey_usage": ExtKeyUsageSyntax,
"microsoft_ctl_friendly_name": CTLString,
# "microsoft_ctl_key_identifier": OctetString,
# "microsoft_ctl_subject_name_md5_hash": OctetString,
# "microsoft_ctl_root_program_cert_policies": ExtKeyUsageSyntax,
# "microsoft_ctl_auth_root_sha256_hash": OctetString,
"microsoft_ctl_disallowed_filetime": FileTime,
# "microsoft_ctl_root_program_chain_policies": ExtKeyUsageSyntax,
"microsoft_ctl_disallowed_enhkey_usage": ExtKeyUsageSyntax,
"microsoft_ctl_not_before_filetime": FileTime,
"microsoft_ctl_not_before_enhkey_usage": ExtKeyUsageSyntax,
}

def _values_spec(self) -> type[Asn1Value] | None:
return self._oid_specs.get(self["type"].native, None)

_spec_callbacks = {"values": _values_spec}


class SubjectAttributes(SetOf): # type: ignore[misc]
_child_spec = SubjectAttribute


class TrustedSubject(Sequence): # type: ignore[misc]
_fields = [
("subject_identifier", SubjectIdentifier),
("subject_attributes", SubjectAttributes, {"optional": True}),
]


class TrustedSubjects(SequenceOf): # type: ignore[misc]
_child_spec = TrustedSubject


class CertificateTrustList(Sequence): # type: ignore[misc]
_fields = [
("version", CTLVersion, {"default": "v1"}),
("subject_usage", SubjectUsage),
("list_identifier", ListIdentifier, {"optional": True}),
("sequence_number", Integer, {"optional": True}),
("ctl_this_update", Time),
("ctl_next_update", Time, {"optional": True}),
("subject_algorithm", DigestAlgorithm),
("trusted_subjects", TrustedSubjects, {"optional": True}),
("ctl_extensions", Extensions, {"optional": True, "explicit": 0}),
]


# Add CTL to acceptable options
ContentType._map["1.3.6.1.4.1.311.10.1"] = "microsoft_ctl"
ContentInfo._oid_specs["microsoft_ctl"] = EncapsulatedContentInfo._oid_specs[
"microsoft_ctl"
] = CertificateTrustList
Loading

0 comments on commit 2feeae8

Please sign in to comment.