# -*- coding: utf-8 -*-
# Copyright (c) 2019 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import annotations
from .base import (
Attestation,
AttestationType,
AttestationResult,
InvalidData,
InvalidSignature,
catch_builtins,
_validate_cert_common,
)
from ..cose import CoseKey
from ..utils import bytes2int, ByteBuffer
from enum import IntEnum, unique
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from cryptography.hazmat.primitives import hashes
from cryptography import x509
from cryptography.exceptions import InvalidSignature as _InvalidSignature
from dataclasses import dataclass
from typing import Tuple, Union, cast
import struct
TPM_ALG_NULL = 0x0010
OID_AIK_CERTIFICATE = x509.ObjectIdentifier("2.23.133.8.3")
[docs]
@unique
class TpmRsaScheme(IntEnum):
RSASSA = 0x0014
RSAPSS = 0x0016
OAEP = 0x0017
RSAES = 0x0015
[docs]
@unique
class TpmAlgAsym(IntEnum):
RSA = 0x0001
ECC = 0x0023
[docs]
@unique
class TpmAlgHash(IntEnum):
SHA1 = 0x0004
SHA256 = 0x000B
SHA384 = 0x000C
SHA512 = 0x000D
def _hash_alg(self) -> hashes.HashAlgorithm:
if self == TpmAlgHash.SHA1:
return hashes.SHA1() # nosec
elif self == TpmAlgHash.SHA256:
return hashes.SHA256()
elif self == TpmAlgHash.SHA384:
return hashes.SHA384()
elif self == TpmAlgHash.SHA512:
return hashes.SHA512()
raise NotImplementedError(f"_hash_alg is not implemented for {self!r}")
[docs]
@dataclass
class TpmsCertifyInfo:
name: bytes
qualified_name: bytes
TPM_GENERATED_VALUE = b"\xffTCG"
TPM_ST_ATTEST_CERTIFY = b"\x80\x17"
[docs]
@dataclass
class TpmsRsaParms:
"""Parse TPMS_RSA_PARMS struct
See:
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
section 12.2.3.5
"""
symmetric: int
scheme: int
key_bits: int
exponent: int
[docs]
@classmethod
def parse(cls, reader, attributes):
symmetric = reader.unpack("!H")
restricted_decryption = attributes & (
ATTRIBUTES.RESTRICTED | ATTRIBUTES.DECRYPT
)
is_restricted_decryption_key = restricted_decryption == (
ATTRIBUTES.DECRYPT | ATTRIBUTES.RESTRICTED
)
if not is_restricted_decryption_key and symmetric != TPM_ALG_NULL:
# if the key is not a restricted decryption key, this field
# shall be set to TPM_ALG_NULL.
raise ValueError("symmetric is expected to be NULL")
# Otherwise should be set to a supported symmetric algorithm, keysize and mode
# TODO(baloo): Should we have non-null value here, do we expect more data?
scheme = reader.unpack("!H")
restricted_sign = attributes & (ATTRIBUTES.RESTRICTED | ATTRIBUTES.SIGN_ENCRYPT)
is_unrestricted_signing_key = restricted_sign == ATTRIBUTES.SIGN_ENCRYPT
if is_unrestricted_signing_key and scheme not in (
TPM_ALG_NULL,
TpmRsaScheme.RSASSA,
TpmRsaScheme.RSAPSS,
):
raise ValueError(
"key is an unrestricted signing key, scheme is "
"expected to be TPM_ALG_RSAPSS, TPM_ALG_RSASSA, "
"or TPM_ALG_NULL"
)
is_restricted_signing_key = restricted_sign == (
ATTRIBUTES.RESTRICTED | ATTRIBUTES.SIGN_ENCRYPT
)
if is_restricted_signing_key and scheme not in (
TpmRsaScheme.RSASSA,
TpmRsaScheme.RSAPSS,
):
raise ValueError(
"key is a restricted signing key, scheme is "
"expected to be TPM_ALG_RSAPSS, or TPM_ALG_RSASSA"
)
is_unrestricted_decryption_key = restricted_decryption == ATTRIBUTES.DECRYPT
if is_unrestricted_decryption_key and scheme not in (
TpmRsaScheme.OAEP,
TpmRsaScheme.RSAES,
TPM_ALG_NULL,
):
raise ValueError(
"key is an unrestricted decryption key, scheme is "
"expected to be TPM_ALG_RSAES, TPM_ALG_OAEP, or "
"TPM_ALG_NULL"
)
if is_restricted_decryption_key and scheme not in (TPM_ALG_NULL,):
raise ValueError(
"key is an restricted decryption key, scheme is "
"expected to be TPM_ALG_NULL"
)
key_bits = reader.unpack("!H")
exponent = reader.unpack("!L")
if exponent == 0:
# When zero, indicates that the exponent is the default of 2^16 + 1
exponent = (2**16) + 1
return cls(symmetric, scheme, key_bits, exponent)
[docs]
class Tpm2bPublicKeyRsa(bytes):
[docs]
@classmethod
def parse(cls, reader: ByteBuffer) -> Tpm2bPublicKeyRsa:
return cls(reader.read(reader.unpack("!H")))
[docs]
@unique
class TpmEccCurve(IntEnum):
"""TPM_ECC_CURVE
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
section 6.4
"""
NONE = 0x0000
NIST_P192 = 0x0001
NIST_P224 = 0x0002
NIST_P256 = 0x0003
NIST_P384 = 0x0004
NIST_P521 = 0x0005
BN_P256 = 0x0010
BN_P638 = 0x0011
SM2_P256 = 0x0020
[docs]
def to_curve(self) -> ec.EllipticCurve:
if self == TpmEccCurve.NONE:
raise ValueError("No such curve")
elif self == TpmEccCurve.NIST_P192:
return ec.SECP192R1()
elif self == TpmEccCurve.NIST_P224:
return ec.SECP224R1()
elif self == TpmEccCurve.NIST_P256:
return ec.SECP256R1()
elif self == TpmEccCurve.NIST_P384:
return ec.SECP384R1()
elif self == TpmEccCurve.NIST_P521:
return ec.SECP521R1()
raise ValueError("curve is not supported", self)
[docs]
@unique
class TpmiAlgKdf(IntEnum):
"""TPMI_ALG_KDF
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
section 9.28
"""
NULL = TPM_ALG_NULL
KDF1_SP800_56A = 0x0020
KDF2 = 0x0021
KDF1_SP800_108 = 0x0022
[docs]
@dataclass
class TpmsEccParms:
symmetric: int
scheme: int
curve_id: TpmEccCurve
kdf: TpmiAlgKdf
[docs]
@classmethod
def parse(cls, reader: ByteBuffer) -> TpmsEccParms:
symmetric = reader.unpack("!H")
scheme = reader.unpack("!H")
if symmetric != TPM_ALG_NULL:
raise ValueError("symmetric is expected to be NULL")
if scheme != TPM_ALG_NULL:
raise ValueError("scheme is expected to be NULL")
curve_id = TpmEccCurve(reader.unpack("!H"))
kdf_scheme = TpmiAlgKdf(reader.unpack("!H"))
return cls(symmetric, scheme, curve_id, kdf_scheme)
[docs]
@dataclass
class TpmsEccPoint:
"""TPMS_ECC_POINT
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
Section 11.2.5.2
"""
x: bytes
y: bytes
[docs]
@classmethod
def parse(cls, reader: ByteBuffer) -> TpmsEccPoint:
x = reader.read(reader.unpack("!H"))
y = reader.read(reader.unpack("!H"))
return cls(x, y)
[docs]
@unique
class ATTRIBUTES(IntEnum):
"""Object attributes
see section 8.3
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
"""
FIXED_TPM = 1 << 1
ST_CLEAR = 1 << 2
FIXED_PARENT = 1 << 4
SENSITIVE_DATA_ORIGIN = 1 << 5
USER_WITH_AUTH = 1 << 6
ADMIN_WITH_POLICY = 1 << 7
NO_DA = 1 << 10
ENCRYPTED_DUPLICATION = 1 << 11
RESTRICTED = 1 << 16
DECRYPT = 1 << 17
SIGN_ENCRYPT = 1 << 18
SHALL_BE_ZERO = (
(1 << 0) # 0 Reserved
| (1 << 3) # 3 Reserved
| (0x3 << 8) # 9:8 Reserved
| (0xF << 12) # 15:12 Reserved
| ((0xFFFFFFFF << 19) & (2**32 - 1)) # 31:19 Reserved
)
_PublicKey = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey]
_Parameters = Union[TpmsRsaParms, TpmsEccParms]
_Unique = Union[Tpm2bPublicKeyRsa, TpmsEccPoint]
def _validate_tpm_cert(cert):
# https://www.w3.org/TR/webauthn/#tpm-cert-requirements
_validate_cert_common(cert)
s = cert.subject.get_attributes_for_oid(x509.NameOID)
if s:
raise InvalidData("Certificate should not have Subject")
s = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
if not s:
raise InvalidData("Certificate should have SubjectAlternativeName")
ext = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
has_aik = [x == OID_AIK_CERTIFICATE for x in ext.value]
if True not in has_aik:
raise InvalidData(
'Extended key usage MUST contain the "joint-iso-itu-t(2) '
"internationalorganizations(23) 133 tcg-kp(8) "
'tcg-kp-AIKCertificate(3)" OID.'
)
[docs]
class TpmAttestation(Attestation):
FORMAT = "tpm"
[docs]
@catch_builtins
def verify(self, statement, auth_data, client_data_hash):
if "ecdaaKeyId" in statement:
raise NotImplementedError("ECDAA not implemented")
alg = statement["alg"]
x5c = statement["x5c"]
cert_info = statement["certInfo"]
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
_validate_tpm_cert(cert)
pub_key = CoseKey.for_alg(alg).from_cryptography_key(cert.public_key())
try:
pub_area = TpmPublicFormat.parse(statement["pubArea"])
except Exception as e:
raise InvalidData("unable to parse pubArea", e)
# Verify that the public key specified by the parameters and unique
# fields of pubArea is identical to the credentialPublicKey in the
# attestedCredentialData in authenticatorData.
if (
auth_data.credential_data.public_key.from_cryptography_key(
pub_area.public_key()
)
!= auth_data.credential_data.public_key
):
raise InvalidSignature(
"attestation pubArea does not match attestedCredentialData"
)
try:
# TpmAttestationFormat.parse is reponsible for:
# Verify that magic is set to TPM_GENERATED_VALUE.
# Verify that type is set to TPM_ST_ATTEST_CERTIFY.
tpm = TpmAttestationFormat.parse(cert_info)
# Verify that extraData is set to the hash of attToBeSigned
# using the hash algorithm employed in "alg".
att_to_be_signed = auth_data + client_data_hash
hash_alg = pub_key._HASH_ALG # type: ignore
digest = hashes.Hash(hash_alg, backend=default_backend())
digest.update(att_to_be_signed)
data = digest.finalize()
if tpm.data != data:
raise InvalidSignature(
"attestation does not sign for authData and ClientData"
)
# Verify that attested contains a TPMS_CERTIFY_INFO structure as
# specified in [TPMv2-Part2] section 10.12.3, whose name field
# contains a valid Name for pubArea, as computed using the
# algorithm in the nameAlg field of pubArea using the procedure
# specified in [TPMv2-Part1] section 16.
# [TPMv2-Part2]:
# https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
# [TPMv2-Part1]:
# https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-1-Architecture-01.38.pdf
if tpm.attested.name != pub_area.name():
raise InvalidData(
"TPMS_CERTIFY_INFO does not include a valid name for pubArea"
)
pub_key.verify(cert_info, statement["sig"])
return AttestationResult(AttestationType.ATT_CA, x5c)
except _InvalidSignature:
raise InvalidSignature("signature of certInfo does not match")