def _get_ext_by_oid(cert, oid): oid = ObjectIdentifier(oid) try: extension = cert.extensions.get_extension_for_oid(oid) return extension.value.value except ExtensionNotFound: return None
# 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 u2flib_server.attestation.matchers import DEFAULT_MATCHERS from u2flib_server.attestation.resolvers import create_resolver from u2flib_server.jsapi import DeviceInfo from u2flib_server.yubicommon.compat import byte2int from cryptography.x509 import ObjectIdentifier, ExtensionNotFound from enum import IntEnum __all__ = ['Attestation', 'MetadataProvider', 'Transport'] TRANSPORTS_EXT_OID = ObjectIdentifier('1.3.6.1.4.1.45724.2.1.1') class Transport(IntEnum): BT_CLASSIC = 0x01 # Bluetooth Classic BLE = 0x02 # Bluetooth Low Energy USB = 0x04 NFC = 0x08 class Attestation(object): def __init__(self, trusted, vendor_info=None, device_info=None, cert_transports=0):
class ExtensionsObjectIds: NODE_TYPE = ObjectIdentifier("1.22.333.4444") NODE_ID = ObjectIdentifier("1.22.333.4445") ACCOUNT_ID = ObjectIdentifier("1.22.333.4446") NODE_PRIVILEGES = ObjectIdentifier("1.22.333.4447")
:raises service_identity.VerificationError: If *certificate* is not valid for *hostname*. :raises service_identity.CertificateError: If *certificate* contains invalid/unexpected data. :returns: ``None`` """ verify_service_identity( cert_patterns=extract_ids(certificate), obligatory_ids=[DNS_ID(hostname)], optional_ids=[], ) ID_ON_DNS_SRV = ObjectIdentifier('1.3.6.1.5.5.7.8.7') # id_on_dnsSRV def extract_ids(cert): """ Extract all valid IDs from a certificate for service verification. If *cert* doesn't contain any identifiers, the ``CN``s are used as DNS-IDs as fallback. :param cryptography.x509.Certificate cert: The certificate to be dissected. :return: List of IDs. """ ids = [] try:
def login(self, message): if self.state != LOGIN: logger.warning("Invalid State") return False logger.info(f"Loging in") data = message.get("data", None) self.state = LOGIN_FINISH status = False # status = False -> if login wasn't a success if AUTH_TYPE == AUTH_MEM: new_otp = base64.b64decode(data["otp"].encode()) current_otp_client = digest(new_otp, "SHA256") message = { "type": "ERROR", "message": "Invalid credentials for logging in" } if self.current_otp == current_otp_client: # success login status = True if self.clear_credentials: logger.info( "Clearing old credentials and saving new ones.") self.current_otp_index = self.new_index + 1 self.current_otp_root = self.new_root new_otp = self.new_otp with open(f"credentials/{self.user_id}_index", "wb") as file: file.write(f"{self.current_otp_index - 1}".encode()) with open(f"credentials/{self.user_id}_root", "wb") as file: file.write(self.current_otp_root) with open(f"credentials/{self.user_id}_otp", "wb") as file: file.write(new_otp) logger.info( "User logged in with success! Credentials updated.") else: logger.info( "User not logged in! Wrong credentials where given.") elif AUTH_TYPE == AUTH_CC: cc_certificate = certificate_object( base64.b64decode(data["certificate"].encode())) signed_nonce = base64.b64decode(data["sign_nonce"].encode()) certificates = load_certificates("cc_certificates/") chain = [] chain_completed = construct_certificate_chain( chain, cc_certificate, certificates) if not chain_completed: error_message = "Couldn't complete the certificate chain" logger.warning(error_message) message = {"type": "ERROR", "message": error_message} status = False else: valid_chain, error_messages = validate_certificate_chain(chain) if not valid_chain: logger.error(error_messages) message = {"type": "ERROR", "message": error_messages} status = False else: status = verify_signature(cc_certificate, signed_nonce, self.nonce) if status: oid = ObjectIdentifier( "2.5.4.5") # oid of citizens card's CI (civil id) self.user_id = cc_certificate.subject.get_attributes_for_oid( oid)[0].value logger.info("User logged in with success") message = {"type": "OK"} # Access verification if status: access_result = self.check_access() if not access_result[0]: logger.warning(access_result[1]) status = False message = {"type": "ERROR", "message": access_result[1]} else: message = {"type": "OK"} logger.info(access_result[1]) self._send(message) return status
def lookup_ec_by_oid(service: IOService = Provide[Container.service]): dotted_string = service.input( "Give the Elliptic Curve's dotted string") return ec.get_curve_for_oid(ObjectIdentifier(dotted_string))
def verify_android_key( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, credential_public_key: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """Verify an "android-key" attestation statement See https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation Also referenced: https://source.android.com/security/keystore/attestation """ if not attestation_statement.sig: raise InvalidRegistrationResponse( "Attestation statement was missing signature (Android Key)") if not attestation_statement.alg: raise InvalidRegistrationResponse( "Attestation statement was missing algorithm (Android Key)") if not attestation_statement.x5c: raise InvalidRegistrationResponse( "Attestation statement was missing x5c (Android Key)") # Validate certificate chain try: # Include known root certificates for this attestation format pem_root_certs_bytes.append(google_hardware_attestation_root_1) pem_root_certs_bytes.append(google_hardware_attestation_root_2) validate_certificate_chain( x5c=attestation_statement.x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (Android Key)") # Extract attStmt bytes from attestation_object attestation_dict = cbor2.loads(attestation_object) authenticator_data_bytes = attestation_dict["authData"] # Generate a hash of client_data_json client_data_hash = hashlib.sha256() client_data_hash.update(client_data_json) client_data_hash_bytes = client_data_hash.digest() verification_data = b"".join([ authenticator_data_bytes, client_data_hash_bytes, ]) # Verify that sig is a valid signature over the concatenation of authenticatorData # and clientDataHash using the public key in the first certificate in x5c with the # algorithm specified in alg. attestation_cert_bytes = attestation_statement.x5c[0] attestation_cert = x509.load_der_x509_certificate(attestation_cert_bytes, default_backend()) attestation_cert_pub_key = attestation_cert.public_key() try: verify_signature( public_key=attestation_cert_pub_key, signature_alg=attestation_statement.alg, signature=attestation_statement.sig, data=verification_data, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (Android Key)") # Verify that the public key in the first certificate in x5c matches the # credentialPublicKey in the attestedCredentialData in authenticatorData. attestation_cert_pub_key_bytes = attestation_cert_pub_key.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) # Convert our raw public key bytes into the same format cryptography generates for # the cert subject key decoded_pub_key = decode_credential_public_key(credential_public_key) pub_key_crypto = decoded_public_key_to_cryptography(decoded_pub_key) pub_key_crypto_bytes = pub_key_crypto.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) if attestation_cert_pub_key_bytes != pub_key_crypto_bytes: raise InvalidRegistrationResponse( "Certificate public key did not match credential public key (Android Key)" ) # Verify that the attestationChallenge field in the attestation certificate # extension data is identical to clientDataHash. ext_key_description_oid = "1.3.6.1.4.1.11129.2.1.17" try: cert_extensions = attestation_cert.extensions ext_key_description: Extension = cert_extensions.get_extension_for_oid( ObjectIdentifier(ext_key_description_oid)) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ext_key_description_oid} (Android Key)" ) # Peel apart the Extension into an UnrecognizedExtension, then the bytes we actually # want ext_value_wrapper: UnrecognizedExtension = ext_key_description.value ext_value: bytes = ext_value_wrapper.value parsed_ext = KeyDescription.load(ext_value) # Verify the following using the appropriate authorization list from the attestation # certificate extension data: software_enforced: AuthorizationList = parsed_ext["softwareEnforced"] tee_enforced: AuthorizationList = parsed_ext["teeEnforced"] # The AuthorizationList.allApplications field is not present on either authorization # list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped # to the RP ID. if software_enforced["allApplications"].native is not None: raise InvalidRegistrationResponse( "allApplications field was present in softwareEnforced (Android Key)" ) if tee_enforced["allApplications"].native is not None: raise InvalidRegistrationResponse( "allApplications field was present in teeEnforced (Android Key)") # The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. origin = tee_enforced["origin"].native if origin != KeyOrigin.GENERATED: raise InvalidRegistrationResponse( f"teeEnforced.origin {origin} was not {KeyOrigin.GENERATED}") # The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. purpose = tee_enforced["purpose"].native if purpose != [KeyPurpose.SIGN]: raise InvalidRegistrationResponse( f"teeEnforced.purpose {purpose} was not [{KeyPurpose.SIGN}]") return True
def _set_policy_identifier(self, value: ParsablePolicyIdentifier) -> None: if isinstance(value, str): self._policy_identifier = ObjectIdentifier(value) else: self._policy_identifier = value
class ExtendedKeyUsage(OrderedSetExtension[x509.ExtendedKeyUsage, Union[ObjectIdentifier, str], str, ObjectIdentifier]): """Class representing a ExtendedKeyUsage extension.""" key = "extended_key_usage" """Key used in CA_PROFILES.""" name: ClassVar[str] = "ExtendedKeyUsage" oid: ClassVar[x509.ObjectIdentifier] = ExtensionOID.EXTENDED_KEY_USAGE CRYPTOGRAPHY_MAPPING = { "serverAuth": ExtendedKeyUsageOID.SERVER_AUTH, "clientAuth": ExtendedKeyUsageOID.CLIENT_AUTH, "codeSigning": ExtendedKeyUsageOID.CODE_SIGNING, "emailProtection": ExtendedKeyUsageOID.EMAIL_PROTECTION, "timeStamping": ExtendedKeyUsageOID.TIME_STAMPING, "OCSPSigning": ExtendedKeyUsageOID.OCSP_SIGNING, "anyExtendedKeyUsage": ExtendedKeyUsageOID.ANY_EXTENDED_KEY_USAGE, "smartcardLogon": ObjectIdentifier("1.3.6.1.4.1.311.20.2.2"), "msKDC": ObjectIdentifier("1.3.6.1.5.2.3.5"), # Defined in RFC 3280, occurs in TrustID Server A52 CA "ipsecEndSystem": ObjectIdentifier("1.3.6.1.5.5.7.3.5"), "ipsecTunnel": ObjectIdentifier("1.3.6.1.5.5.7.3.6"), "ipsecUser": ObjectIdentifier("1.3.6.1.5.5.7.3.7"), } _CRYPTOGRAPHY_MAPPING_REVERSED = { v: k for k, v in CRYPTOGRAPHY_MAPPING.items() } KNOWN_PARAMETERS = sorted(CRYPTOGRAPHY_MAPPING) """Known values that can be passed to this extension.""" # Used by the HTML form select field CHOICES = ( ("serverAuth", "SSL/TLS Web Server Authentication"), ("clientAuth", "SSL/TLS Web Client Authentication"), ("codeSigning", "Code signing"), ("emailProtection", "E-mail Protection (S/MIME)"), ("timeStamping", "Trusted Timestamping"), ("OCSPSigning", "OCSP Signing"), ("smartcardLogon", "Smart card logon"), ("msKDC", "Kerberos Domain Controller"), ("ipsecEndSystem", "IPSec EndSystem"), ("ipsecTunnel", "IPSec Tunnel"), ("ipsecUser", "IPSec User"), ("anyExtendedKeyUsage", "Any Extended Key Usage"), ) def from_extension(self, value: x509.ExtendedKeyUsage) -> None: self.value = set(value) @property def extension_type(self) -> x509.ExtendedKeyUsage: # call serialize_item() to ensure consistent sort order return x509.ExtendedKeyUsage( sorted(self.value, key=self.serialize_item)) def serialize_item(self, value: x509.ObjectIdentifier) -> str: return self._CRYPTOGRAPHY_MAPPING_REVERSED[value] def parse_value(self, value: Union[ObjectIdentifier, str]) -> ObjectIdentifier: if isinstance(value, ObjectIdentifier ) and value in self._CRYPTOGRAPHY_MAPPING_REVERSED: return value if isinstance(value, str) and value in self.CRYPTOGRAPHY_MAPPING: return self.CRYPTOGRAPHY_MAPPING[value] raise ValueError("Unknown value: %s" % value)
def makepdf(self, prev, udct, algomd, zeros, cert, **params): catalog = prev.trailer["/Root"] size = prev.trailer["/Size"] pages = catalog["/Pages"].getObject() page0ref = prev.getPage(udct.get("sigpage", 0)).indirectRef self._objects = [] while len(self._objects) < size - 1: self._objects.append(None) ## if params['mode'] == 'timestamp': ## # deal with extensions ## if '/Extensions' not in catalog: ## extensions = po.DictionaryObject() ## else: ## extensions = catalog['/Extensions'] ## ## if '/ESIC' not in extensions: ## extensions.update({ ## po.NameObject("/ESIC"): po.DictionaryObject({ ## po.NameObject('/BaseVersion'): po.NameObject('/1.7'), ## po.NameObject('/ExtensionLevel'): po.NumberObject(1), ## }) ## }) ## else: ## esic = extensions['/ESIC'] ## major, minor = esic['/BaseVersion'].lstrip('/').split('.') ## if int(major) < 1 or int(minor) < 7: ## esic.update({ ## po.NameObject('/BaseVersion'): po.NameObject('/1.7'), ## po.NameObject('/ExtensionLevel'): po.NumberObject(1), ## }) ## catalog.update({ ## po.NameObject('/Extensions'): extensions ## }) # obj12 is the digital signature obj12, obj12ref = self._make_signature( Type=po.NameObject("/Sig"), SubFilter=po.NameObject("/adbe.pkcs7.detached"), Contents=UnencryptedBytes(zeros), ) if params['mode'] == 'timestamp': # obj12 is a timestamp this time obj12.update({ po.NameObject("/Type"): po.NameObject("/DocTimeStamp"), po.NameObject("/SubFilter"): po.NameObject("/ETSI.RFC3161"), po.NameObject("/V"): po.NumberObject(0), }) else: obj12.update({ po.NameObject("/Name"): po.createStringObject(udct["contact"]), po.NameObject("/Location"): po.createStringObject(udct["location"]), po.NameObject("/Reason"): po.createStringObject(udct["reason"]), }) if params.get('use_signingdate'): obj12.update({ po.NameObject("/M"): po.createStringObject(udct["signingdate"]), }) # obj13 is a combined AcroForm Sig field with Widget annotation new_13 = True #obj13 = po.DictionaryObject() if udct.get('signform', False): # Attaching signature to existing field in AcroForm if "/AcroForm" in catalog: form = catalog["/AcroForm"].getObject() if "/Fields" in form: fields = form["/Fields"].getObject() obj13ref = [ f for f in fields if f.getObject()['/T'] == udct.get( 'sigfield', 'Signature1') ][0] obj13 = obj13ref.getObject() self._objects[obj13ref.idnum - 1] = obj13 new_13 = False # box is coordinates of the annotation to fill box = udct.get("signaturebox", None) if new_13: obj13, obj13ref = self._make_sig_annotation( F=po.NumberObject(udct.get("sigflagsft", 132)), T=EncodedString(udct.get("sigfield", "Signature1")), Vref=obj12ref, Pref=page0ref, ) else: # original obj13 is a merged SigField/SigAnnot # Setting /V on the AcroForm field sets the signature # for the field obj13.update({ po.NameObject("/V"): obj12ref, }) # fill the existing signature field annotation, # ignore any other location if "/Rect" in obj13: box = [float(f) for f in obj13["/Rect"]] # add an annotation if there is a field to fill if box is not None: from endesive.pdf.PyPDF2_annotate.annotations.signature import Signature from endesive.pdf.PyPDF2_annotate.config.appearance import Appearance from endesive.pdf.PyPDF2_annotate.config.location import Location from endesive.pdf.PyPDF2_annotate.util.geometry import identity x1, y1, x2, y2 = box annotation = Signature( Location(x1=x1, y1=y1, x2=x2, y2=y2, page=0), Appearance(), ) if 'signature' in udct: # Plain text signature with the default font # text to render is contained in udct['signature'] # font parameters are in udct['signature']['text'] annotationtext = udct["signature"] wrap_text = udct.get('text', {}).get('wraptext', True) font_size = udct.get('text', {}).get('fontsize', 12) text_align = udct.get('text', {}).get('textalign', 'left') line_spacing = udct.get('text', {}).get('linespacing', 1.2) annotation.add_default_font() annotation.set_signature_appearance( ['fill_colour', 0, 0, 0], ['font', 'default', font_size], [ 'text_box', annotationtext, 'default', 0, 0, x2 - x1, y2 - y1, font_size, wrap_text, text_align, 'middle', line_spacing ]) elif 'signature_img' in udct: # Simple image signature, stretches to fit the box # image to render is contained in udct['signature_image'] annotation.add_image(udct["signature_img"], "Image") annotation.set_signature_appearance([ 'image', "Image", 0, 0, x2 - x1, y2 - y1, udct.get('signature_img_distort', True), udct.get('signature_img_centred', False), ]) elif 'signature_appearance' in udct: # Adobe-inspired signature with text and images # Parameters are contained in udct['signature_appearance'] # If a field is included in the display list, that field # will be contained in the annotation. # # Text and border are the colour specified by outline, # and border is the the inset distance from the outer # edge of the annotation. The R G B values range between # 0 and 1. # # Icon is an image to display above the background and # border at the left-hand side of the anntoation. If # there is no text, it is centred. # # The text block is left-aligned to the right of the icon # image. If there is no image, the text is left-aliged # with the left-hand border of the annotation # # display fields: # CN, DN, date, contact, reason, location # # Dict format: # appearance = dict( # background = Image with alpha / None, # icon = Image with alpha / None, # labels = bool, # display = list, # software = str, # outline = [R, G, B], # border = int, # ) sig = {} for f in ('background', 'icon', 'labels', 'border', 'outline'): if f in udct['signature_appearance']: sig[f] = udct['signature_appearance'][f] toggles = udct['signature_appearance'].get('display', []) for f in ('contact', 'reason', 'location', 'contact', 'signingdate'): if f in toggles: sig[f] = udct.get(f, '{} unknown'.format(f)) if 'date' in toggles: sig['date'] = udct['signingdate'] if 'CN' in toggles: from cryptography.x509 import ObjectIdentifier sig['CN'] = cert.subject.get_attributes_for_oid( ObjectIdentifier('2.5.4.3'))[0].value if 'DN' in toggles: sig['DN'] = cert.subject.rfc4514_string() annotation.simple_signature(sig) else: # Manual signature annotation creation # # Make your own appearance with an arbitrary number of # images and fonts if 'manual_images' in udct: for name, img in udct['manual_images'].items(): annotation.add_image(img, name=name) if 'manual_fonts' in udct: for name, path in udct['manual_fonts'].items(): annotation.add_ttf_font(path, name=name) annotation.add_default_font() annotation.set_signature_appearance(*udct['signature_manual']) pdfa = annotation.as_pdf_object(identity(), page=page0ref) objapn = self._extend(pdfa["/AP"]["/N"]) objapnref = self._addObject(objapn) objap = po.DictionaryObject() objap[po.NameObject("/N")] = objapnref obj13.update({ po.NameObject("/Rect"): po.ArrayObject([ po.FloatObject(x1), po.FloatObject(y1), po.FloatObject(x2), po.FloatObject(y2), ]), po.NameObject("/AP"): objap, #po.NameObject("/SM"): po.createStringObject("TabletPOSinline"), }) page0 = page0ref.getObject() if new_13: annots = po.ArrayObject([obj13ref]) if "/Annots" in page0: page0annots = page0["/Annots"] if isinstance(page0annots, po.IndirectObject): annots.insert(0, page0annots) elif isinstance(page0annots, po.ArrayObject): annots = page0annots annots.append(obj13ref) else: annots = page0["/Annots"] page0.update({po.NameObject("/Annots"): annots}) self._objects[page0ref.idnum - 1] = page0 if udct.get("sigandcertify", False) and "/Perms" not in catalog: obj10 = po.DictionaryObject() obj10ref = self._addObject(obj10) obj11 = po.DictionaryObject() obj11ref = self._addObject(obj11) obj14 = po.DictionaryObject() obj14ref = self._addObject(obj14) obj14.update({po.NameObject("/DocMDP"): obj12ref}) obj10.update({ po.NameObject("/Type"): po.NameObject("/TransformParams"), po.NameObject("/P"): po.NumberObject(udct.get("sigflags", 3)), po.NameObject("/V"): po.NameObject("/1.2"), }) obj11.update({ po.NameObject("/Type"): po.NameObject("/SigRef"), po.NameObject("/TransformMethod"): po.NameObject("/DocMDP"), po.NameObject("/DigestMethod"): po.NameObject("/" + algomd.upper()), po.NameObject("/TransformParams"): obj10ref, }) obj12[po.NameObject("/Reference")] = po.ArrayObject([obj11ref]) catalog[po.NameObject("/Perms")] = obj14ref if "/AcroForm" in catalog: form = catalog["/AcroForm"].getObject() if "/Fields" in form: fields = form["/Fields"] old_field_names = [f.getObject()['/T'] for f in fields] else: fields = po.ArrayObject() old_field_names = [] if udct.get('auto_sigfield', False) and obj13['/T'] in old_field_names: name_base = udct.get('sigfield', 'Signature1') checklist = [ f[len(name_base):] for f in old_field_names if f.startswith(name_base) ] for i in range(1, len(checklist) + 1): suffix = '_{}'.format(i) if suffix in checklist: next new_name = '{}{}'.format(name_base, suffix) obj13.update( {po.NameObject("/T"): EncodedString(new_name)}) break old_flags = int(form.get("/SigFlags", 0)) new_flags = int(form.get("/SigFlags", 0)) | udct.get("sigflags", 3) if new_13: fields.append(obj13ref) form.update({ po.NameObject("/Fields"): fields, po.NameObject("/SigFlags"): po.NumberObject(new_flags), }) elif new_flags > old_flags: form.update({ po.NameObject("/SigFlags"): po.NumberObject(new_flags), }) formref = catalog.raw_get("/AcroForm") if isinstance(formref, po.IndirectObject): self._objects[formref.idnum - 1] = form form = formref else: form = po.DictionaryObject() form.update({ po.NameObject("/Fields"): po.ArrayObject([obj13ref]), po.NameObject("/SigFlags"): po.NumberObject(udct.get("sigflags", 3)), }) catalog[po.NameObject("/AcroForm")] = form if "/Metadata" in catalog: catalog[po.NameObject("/Metadata")] = catalog.raw_get("/Metadata") x_root = prev.trailer.raw_get("/Root") self._objects[x_root.idnum - 1] = catalog self.x_root = po.IndirectObject(x_root.idnum, 0, self) self.x_info = prev.trailer.get("/Info")
# General Public License for more details. # # You should have received a copy of the GNU General Public License along with django-ca. If not, # see <http://www.gnu.org/licenses/>. # pylint: disable=attribute-defined-outside-init; https://github.com/PyCQA/pylint/issues/3605 # pylint: disable=missing-function-docstring; https://github.com/PyCQA/pylint/issues/3605 """Module providing extensions for x509 authorities supporting OpenSSH.""" import typing from cryptography.x509 import Extension from cryptography.x509 import ObjectIdentifier from cryptography.x509 import UnrecognizedExtension SSH_HOST_CA = ObjectIdentifier("1.2.22.2") SSH_USER_CA = ObjectIdentifier("1.2.22.1") if typing.TYPE_CHECKING: SshHostCaExtensionBase = Extension["SshHostCaType"] SshUserCaExtensionBase = Extension["SshUserCaType"] else: SshHostCaExtensionBase = SshUserCaExtensionBase = Extension class SshHostCaType(UnrecognizedExtension): """ CA Certs with this extension can sign OpenSSH Host keys. """ def __init__(self) -> None: super().__init__(SSH_HOST_CA, b"OpenSSH Host CA")
def verify_apple( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, credential_public_key: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """ https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation """ if not attestation_statement.x5c: raise InvalidRegistrationResponse( "Attestation statement was missing x5c (Apple)") # Validate the certificate chain try: # Include known root certificates for this attestation format pem_root_certs_bytes.append(apple_webauthn_root_ca) validate_certificate_chain( x5c=attestation_statement.x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (Apple)") # Concatenate authenticatorData and clientDataHash to form nonceToHash. attestation_dict = cbor2.loads(attestation_object) authenticator_data_bytes = attestation_dict["authData"] client_data_hash = hashlib.sha256() client_data_hash.update(client_data_json) client_data_hash_bytes = client_data_hash.digest() nonce_to_hash = b"".join([ authenticator_data_bytes, client_data_hash_bytes, ]) # Perform SHA-256 hash of nonceToHash to produce nonce. nonce = hashlib.sha256() nonce.update(nonce_to_hash) nonce_bytes = nonce.digest() # Verify that nonce equals the value of the extension with # OID 1.2.840.113635.100.8.2 in credCert. attestation_cert_bytes = attestation_statement.x5c[0] attestation_cert = x509.load_der_x509_certificate(attestation_cert_bytes, default_backend()) cert_extensions = attestation_cert.extensions # Still no documented name for this OID... ext_1_2_840_113635_100_8_2_oid = "1.2.840.113635.100.8.2" try: ext_1_2_840_113635_100_8_2: Extension = cert_extensions.get_extension_for_oid( ObjectIdentifier(ext_1_2_840_113635_100_8_2_oid)) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ext_1_2_840_113635_100_8_2_oid} (Apple)" ) # Peel apart the Extension into an UnrecognizedExtension, then the bytes we actually # want ext_value_wrapper: UnrecognizedExtension = ext_1_2_840_113635_100_8_2.value # Ignore the first six ASN.1 structure bytes that define the nonce as an # OCTET STRING. Should trim off '0$\xa1"\x04' ext_value: bytes = ext_value_wrapper.value[6:] if ext_value != nonce_bytes: raise InvalidRegistrationResponse( "Certificate nonce was not expected value (Apple)") # Verify that the credential public key equals the Subject Public Key of credCert. attestation_cert_pub_key = attestation_cert.public_key() attestation_cert_pub_key_bytes = attestation_cert_pub_key.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) # Convert our raw public key bytes into the same format cryptography generates for # the cert subject key decoded_pub_key = decode_credential_public_key(credential_public_key) pub_key_crypto = decoded_public_key_to_cryptography(decoded_pub_key) pub_key_crypto_bytes = pub_key_crypto.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) if attestation_cert_pub_key_bytes != pub_key_crypto_bytes: raise InvalidRegistrationResponse( "Certificate public key did not match credential public key (Apple)" ) return True