Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for smartcard identities #1181

Merged
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
118 changes: 115 additions & 3 deletions tools/codesigningtool/codesigningtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# limitations under the License.

import argparse
import base64
import datetime
import os
import plistlib
import re
Expand Down Expand Up @@ -120,16 +122,122 @@ def _certificate_fingerprint(identity):
fingerprint = fingerprint.replace(":", "")
return fingerprint

def _certificate_common_name(cert):
_, subject, _ = execute.execute_and_filter_output([
"openssl",
thiagohmcruz marked this conversation as resolved.
Show resolved Hide resolved
"x509",
"-noout",
"-inform",
"DER",
"-subject"
], inputstr=cert, raise_on_failure=True)
subject = subject.strip().split('/')
cert_cn = [f for f in subject if "CN=" in f]

if len(cert_cn) == 0:
return None

cert_cn = cert_cn[0]
cert_cn = cert_cn.replace("CN=", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible this could be None ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should assume it's possible. I'll add the check instead of accessing [0] here and make it return None. Tested locally and is all good since at the call site the comparison is if identity == common_name:


return cert_cn

def _get_identities_from_provisioning_profile(mpf):
"""Iterates through all the identities in a provisioning profile, lazily."""
for identity in mpf["DeveloperCertificates"]:
if not isinstance(identity, bytes):
cert = _certificate_data(identity)
yield _certificate_fingerprint(cert)

def _certificate_data(cert):
if not isinstance(cert, bytes):
# Old versions of plistlib return the deprecated plistlib.Data type
# instead of bytes.
identity = identity.data
yield _certificate_fingerprint(identity)
cert = cert.data

return cert
thiagohmcruz marked this conversation as resolved.
Show resolved Hide resolved

def _get_smartcard_tokens(xml):
"""Get available tokens from the output of 'system_profiler SPSmartCardsDataType -xml'"""
tokens = [x for x in xml if x.get("_name", None) == "AVAIL_SMARTCARDS_TOKEN"]

if len(tokens) == 0:
return []

tokens = tokens[0].get("_items", None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is only checking the first match an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the output it's not an issue, all tokens will be located under _items

tokens = [x.get("_name", None) for x in tokens]

return tokens

def _get_smartcard_keychain(xml):
"""Get keychain items from the output of 'system_profiler SPSmartCardsDataType -xml'"""
keychain = [x for x in xml if x.get("_name", None) == "AVAIL_SMARTCARDS_KEYCHAIN"]

if len(keychain) == 0:
return []

keychain = keychain[0].get("_items", None)
return keychain

def _find_smartcard_identities(identity=None):
"""Finds smartcard identitites on the current system."""
ids = []
_, xml, _ = execute.execute_and_filter_output([
"/usr/sbin/system_profiler",
"SPSmartCardsDataType",
"-xml"
], raise_on_failure=True)
brentleyjones marked this conversation as resolved.
Show resolved Hide resolved
xml = plistlib.loads(str.encode(xml))
if len(xml) == 0:
return []
xml = xml[0].get("_items", None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to consider all entries in here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the top level _items that contains all the info we need. I think it's safe to ignore the rest for the purpose of this function

if not xml:
return []

tokens = _get_smartcard_tokens(xml)
keychain = _get_smartcard_keychain(xml)

# For each 'token' finds non-expired certs and:
#
# 1. Check if 'identity' was provided and if it matches a 'CN', in that case stop the loop
# and return the respective fingerprint (SHA1)
# 2. Otherwise append fingerprints found to 'ids' to be returned at the end
#
# ps: note that if 'identity' is provided and it does not match any existing item in the
# smartcard keychain 'ids' will be empty, making this function's behaviour consistent with
# '_find_codesign_identities' where it's being called
for token in tokens:
token_data = [x for x in keychain if x.get("_name", None) == token]
if len(token_data) == 0:
continue
token_data = token_data[0]

for (k, data) in token_data.items():
if k == "_name":
continue
# Extract expiry date and ignore expired certs. The row being processed looks like this:
#
# Valid from: 2021-02-12 21:35:04 +0000 to: 2022-02-12 21:35:05 +0000, SSL trust: NO, X509 trust: YES
#
expiry_date = re.search(r"(?<=to:)(.*?)(?=,)", data, re.DOTALL).group().strip()
expiry_date = datetime.datetime.strptime(expiry_date, "%Y-%m-%d %H:%M:%S %z")
now = datetime.datetime.now(expiry_date.tzinfo)
if now > expiry_date:
continue

# This is a valid identity, decode the certificate, extract
# Common Name and Fingerprint and handle their values accordingly
# as described above
cert = re.search(r"(?<=-----BEGIN CERTIFICATE-----)(.*?)(?=-----END CERTIFICATE-----)", data, re.DOTALL).group().strip()
cert = base64.b64decode(cert)
thiagohmcruz marked this conversation as resolved.
Show resolved Hide resolved
cert = _certificate_data(cert)
common_name = _certificate_common_name(cert)
fingerprint = _certificate_fingerprint(cert)
if identity == common_name:
return [fingerprint]
if not identity:
ids.append(fingerprint)
thiagohmcruz marked this conversation as resolved.
Show resolved Hide resolved

return ids

def _find_codesign_identities(identity=None):
"""Finds code signing identities on the current system."""
Expand All @@ -156,6 +264,10 @@ def _find_codesign_identities(identity=None):
groups = m.groupdict()
id = groups["hash"]
ids.append(id)

# Finds smartcard identities if present
ids += _find_smartcard_identities(identity)

return ids


Expand Down