Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@ on:

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.12]
python-version: [3.13]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install pipenv
run: |
python -m pip install --upgrade pipenv wheel
- id: cache-pipenv
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
Expand Down
8 changes: 4 additions & 4 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ twine = "*"

[packages]
cbor2 = "*"
certvalidator = "*"
pyhanko-certvalidator = "0.19.8"
pyjwt = "*"
"python-jose[cryptography]" = "==3.3.*"
cryptography = "*"
"python-jose[cryptography]" = "==3.5.*"
cryptography = "==45.0.*"
black = "*"

[scripts]
Expand All @@ -22,4 +22,4 @@ coverage = "pytest --cov=pyattest tests/"
upload = "python setup.py upload"

[requires]
python_version = "3.12"
python_version = "3.13"
1,117 changes: 639 additions & 478 deletions Pipfile.lock

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions pyattest/configs/apple.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from pathlib import Path

from asn1crypto.x509 import Certificate

from pyattest.configs.config import Config
from pyattest.verifiers.apple_assertion import AppleAssertionVerifier
from pyattest.verifiers.apple_attestation import AppleAttestationVerifier
from pyattest.verifiers.utils import _load_certificate


class AppleConfig(Config):
Expand All @@ -23,14 +26,16 @@ def oid(self) -> str:
return "1.2.840.113635.100.8.2"

@property
def root_ca(self) -> bytes:
def root_ca(self) -> Certificate:
"""
Apples App Attestation Root CA. This can be overwritten for easier testing.

See also: https://www.apple.com/certificateauthority/private/
"""
if self._custom_root_ca:
return self._custom_root_ca
bytes = self._custom_root_ca
else:
folder = Path(__file__).parent / "../certificates"
bytes = Path(folder / "Apple_App_Attestation_Root_CA.pem").read_bytes()

folder = Path(__file__).parent / "../certificates"
return Path(folder / "Apple_App_Attestation_Root_CA.pem").read_bytes()
return _load_certificate(bytes)
9 changes: 6 additions & 3 deletions pyattest/configs/google.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import Optional, List

from asn1crypto.x509 import Certificate

from pyattest.configs.config import Config
from pyattest.verifiers.google_assertion import GoogleAssertionVerifier
from pyattest.verifiers.google_attestation import GoogleAttestationVerifier
from pyattest.verifiers.utils import _load_certificate


class GoogleConfig(Config):
Expand All @@ -15,15 +18,15 @@ def __init__(
apk_package_name: str,
production: bool,
root_cn: str = "attest.android.com",
root_ca: bytes = None,
root_ca: Optional[bytes] = None,
):
self.key_ids = key_ids
self.apk_package_name = apk_package_name
self.production = production
self.root_cn = root_cn
self._custom_root_ca = root_ca
self._custom_root_ca = _load_certificate(root_ca) if root_ca else None

@property
def root_ca(self) -> Optional[bytes]:
def root_ca(self) -> Optional[Certificate]:
"""This is only used for simplified unit testing."""
return self._custom_root_ca
15 changes: 9 additions & 6 deletions pyattest/verifiers/apple_attestation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from hashlib import sha256

from asn1crypto.x509 import Certificate, Extension
from certvalidator import CertificateValidator, ValidationContext
from certvalidator.errors import PathValidationError, PathBuildingError
from certvalidator.path import ValidationPath
from pyhanko_certvalidator import CertificateValidator, ValidationContext
from pyhanko_certvalidator.errors import PathValidationError, PathBuildingError
from pyhanko_certvalidator.path import ValidationPath

from pyattest.exceptions import (
ExtensionNotFoundException,
Expand All @@ -19,6 +19,8 @@
from pyattest.verifiers.attestation import AttestationVerifier
from cbor2 import loads as cbor_decode

from pyattest.verifiers.utils import _load_certificate


class AppleAttestationVerifier(AttestationVerifier):
def verify(self):
Expand Down Expand Up @@ -150,17 +152,18 @@ def verify_nonce(self, auth_data: bytes, nonce: bytes, cert: Certificate):
if calculated_nonce != expected_nonce:
raise InvalidNonceException

def verify_certificate_chain(self, chain: list) -> ValidationPath:
def verify_certificate_chain(self, chain: list[bytes]) -> ValidationPath:
"""
Verify that the x5c array contains the intermediate and leaf certificates for App Attest, starting from the
credential certificate stored in the first data buffer in the array (credcert). Verify the validity of the
certificates using Apple’s App Attest root certificate.

See also: https://www.apple.com/certificateauthority/private/
"""
cert = chain.pop(0)
cert =_load_certificate(chain.pop(0))
context = ValidationContext(extra_trust_roots=[self.attestation.config.root_ca])
validator = CertificateValidator(cert, chain, validation_context=context)
chain_certs = [_load_certificate(i) for i in chain]
validator = CertificateValidator(cert, chain_certs, validation_context=context)

try:
return validator.validate_usage({"digital_signature"})
Expand Down
15 changes: 10 additions & 5 deletions pyattest/verifiers/google_attestation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
from typing import List, Optional, Tuple

import jwt
from asn1crypto import pem
from certvalidator import CertificateValidator, ValidationContext
from certvalidator.errors import PathValidationError, PathBuildingError
from certvalidator.path import ValidationPath
from asn1crypto import pem, x509

from pyhanko_certvalidator import ValidationContext, CertificateValidator
from pyhanko_certvalidator.errors import PathValidationError, PathBuildingError
from pyhanko_certvalidator.path import ValidationPath
from jwt import InvalidTokenError

from pyattest.exceptions import (
Expand All @@ -18,6 +19,7 @@
InvalidKeyIdException,
)
from pyattest.verifiers.attestation import AttestationVerifier
from pyattest.verifiers.utils import _load_certificate


class GoogleAttestationVerifier(AttestationVerifier):
Expand Down Expand Up @@ -109,8 +111,11 @@ def verify_certificate_chain(self, chain: List[bytes]) -> ValidationPath:
extra_trust_roots=[self.attestation.config.root_ca]
)

cert = _load_certificate(chain.pop(0))
intermediates = [_load_certificate(i) for i in chain]

validator = CertificateValidator(
chain[0], chain[1:], validation_context=context
cert, intermediates, validation_context=context
)

try:
Expand Down
9 changes: 9 additions & 0 deletions pyattest/verifiers/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from asn1crypto import pem
from asn1crypto.x509 import Certificate


def _load_certificate(cert_bytes: bytes) -> Certificate:
if pem.detect(cert_bytes):
_, _, cert_bytes = pem.unarmor(cert_bytes)

return Certificate.load(cert_bytes)
2 changes: 1 addition & 1 deletion tests/test_google_play_integrity_api_attestation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import base64
import os

from _pytest.python_api import raises
from pytest import raises
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

from pyattest.attestation import Attestation
Expand Down