Skip to content

Conversation

@Micah-Kolide
Copy link
Contributor

This PR adds a new table for darwin devices, the certificate_trust_settings table to acquire the configured cert trust settings of each certificate domain for increased visibility into the status of installed certificates. This data is similar to executing security dump-trust-settings -d (for the 'admin' domain), or security dump-trust-settings -s (for the 'system' domain).

Something that security fails to do, is correctly report the configured trust settings of MDM distributed certificates. At least in some cases, it will report that 0 settings are configured regardless of which settings are set up.

For this table I'm implementing Apple's Security framework to copy the certificate trust settings for a cert domain, and enumerate the returned data.

When I was building the table, I could see that the array returned from SecTrustSettingsCopyTrustSettings included the trust setting policy name: kSecTrustSettingsPolicyName, but when building the table it would fail for the use of an undeclared identifier. This is why I am manually defining this key identifier.

Here is some redacted output:

osquery> SELECT * FROM certificate_trust_settings WHERE trust_domain = 'admin';
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
| common_name                                                      | serial_number                      | trust_domain | trust_policy_name | trust_policy_data | trust_allowed_error | trust_key_usage | trust_result |
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
| Charles Proxy CA (13 Sep 2024, )        | 000000000000                       | admin        | sslServer         |                   | -2147408896         |                 | trusted_root |
| Zscaler Root CA                                                  | 000000000000000000                 | admin        | sslServer         |                   | -2147408896         |                 | trusted_root |
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+

@Micah-Kolide Micah-Kolide requested review from a team as code owners November 20, 2025 17:37
@zwass
Copy link
Member

zwass commented Dec 3, 2025

@Micah-Kolide can you please rebase/merge for macOS CI fixes?

@Micah-Kolide
Copy link
Contributor Author

@Micah-Kolide can you please rebase/merge for macOS CI fixes?

Will do!

@Micah-Kolide Micah-Kolide force-pushed the micah/add_sec_calls_to_cert_table_darwin branch from 52cbcea to ee1c446 Compare December 4, 2025 19:46
Copy link
Member

@zwass zwass left a comment

Choose a reason for hiding this comment

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

Generally looking good though I have requested some changes.

@Micah-Kolide Micah-Kolide requested a review from zwass December 5, 2025 19:30
@zwass
Copy link
Member

zwass commented Dec 9, 2025

@Micah-Kolide on my system, trust_policy_name | trust_policy_data | trust_allowed_error | trust_key_usage | trust_result are all empty for all the rows. Is that expected? Do you know a way I can test these with nonempty values?

@Micah-Kolide
Copy link
Contributor Author

Micah-Kolide commented Dec 10, 2025

@Micah-Kolide on my system, trust_policy_name | trust_policy_data | trust_allowed_error | trust_key_usage | trust_result are all empty for all the rows. Is that expected? Do you know a way I can test these with nonempty values?

Hey @zwass, does running the below commands generate any output?

/usr/bin/security dump-trust-settings -s | awk '/trust settings : [1-9]/{print prev_line; print} {prev_line=$0}'
/usr/bin/security dump-trust-settings -d | awk '/trust settings : [1-9]/{print prev_line; print} {prev_line=$0}'

Mine looks like this (minus the *'s):

$ /usr/bin/security dump-trust-settings -s | awk '/trust settings : [1-9]/{print prev_line; print} {prev_line=$0}'
$ /usr/bin/security dump-trust-settings -d | awk '/trust settings : [1-9]/{print prev_line; print} {prev_line=$0}'
Cert 0: Charles Proxy CA (13 Sep 2024, ***********************)
   Number of trust settings : 2
Cert 1: Zscaler Root CA
   Number of trust settings : 2

If there are any trust settings configured, then removing the awk will show you the other variables.

   Trust Setting 0:
      Policy OID            : SSL
      Allowed Error         : CSSMERR_TP_CERT_EXPIRED
      Result Type           : kSecTrustSettingsResultTrustRoot
   Trust Setting 1:
      Policy OID            : SSL
      Allowed Error         : Host name mismatch
      Result Type           : kSecTrustSettingsResultTrustRoot

I edited the trust settings in my keychain on the Zscaler Root CA cert for these to show up, but I've also tested this by using mkcert for a local CA and using that for my trust setting changes.

Copy link
Member

@zwass zwass left a comment

Choose a reason for hiding this comment

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

I installed Charles Proxy and was able see the trust settings.

When I got this to start returning data, it brought up a number of questions for me.

I'm pretty confident that we need to be emitting one row for each of the trust settings entries (see comment below).

Also,

Should we emit a row when there are no trust settings configured? I think maybe not? But I'm not sure exactly how the table would be used. Certificates with no configured trust settings do already show up in the certificates table.

On my machine I see several entries with no common name. This gives me some concern. Any idea what that's about?

+-------------+----------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
| common_name | serial                           | trust_domain | trust_policy_name | trust_policy_data | trust_allowed_error | trust_key_usage | trust_result |
+-------------+----------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
|             | 15C8BD65475CAFB897005EE406D2BC9D | system       |                   |                   |                     |                 |              |
|             | 5D938D306736C8061D1AC754846907   | system       |                   |                   |                     |                 |              |
|             | 00                               | system       |                   |                   |                     |                 |              |
|             | 00                               | system       |                   |                   |                     |                 |              |
|             | 00                               | system       |                   |                   |                     |                 |              |
|             | 110034B64EC6362D36               | system       |                   |                   |                     |                 |              |
|             | 200605167002                     | system       |                   |                   |                     |                 |              |
+-------------+----------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+

I also never saw any values for trust_policy_data and trust_key_usage even though it does seem like you used them according to the documentation.

@Micah-Kolide
Copy link
Contributor Author

Micah-Kolide commented Dec 11, 2025

I installed Charles Proxy and was able see the trust settings.

When I got this to start returning data, it brought up a number of questions for me.

I'm pretty confident that we need to be emitting one row for each of the trust settings entries (see comment below).

Also,

Should we emit a row when there are no trust settings configured? I think maybe not? But I'm not sure exactly how the table would be used. Certificates with no configured trust settings do already show up in the certificates table.

On my machine I see several entries with no common name. This gives me some concern. Any idea what that's about?

+-------------+----------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
| common_name | serial                           | trust_domain | trust_policy_name | trust_policy_data | trust_allowed_error | trust_key_usage | trust_result |
+-------------+----------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
|             | 15C8BD65475CAFB897005EE406D2BC9D | system       |                   |                   |                     |                 |              |
|             | 5D938D306736C8061D1AC754846907   | system       |                   |                   |                     |                 |              |
|             | 00                               | system       |                   |                   |                     |                 |              |
|             | 00                               | system       |                   |                   |                     |                 |              |
|             | 00                               | system       |                   |                   |                     |                 |              |
|             | 110034B64EC6362D36               | system       |                   |                   |                     |                 |              |
|             | 200605167002                     | system       |                   |                   |                     |                 |              |
+-------------+----------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+

I also never saw any values for trust_policy_data and trust_key_usage even though it does seem like you used them according to the documentation.

You are very right about returning one result per trust setting, and I feel bad for missing that. I have updated the table results to have one per trust setting, and omit certificates without trust settings.

osquery> SELECT * FROM certificate_trust_settings;
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
| common_name                                                      | serial                             | trust_domain | trust_policy_name | trust_policy_data | trust_allowed_error | trust_key_usage | trust_result |
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+
| Charles Proxy CA (13 Sep 2024, )        | 000000000000                       | admin        | sslServer         |                   | -2147409654         |                 | trusted_root |
| Charles Proxy CA (13 Sep 2024, )        | 000000000000                       | admin        | sslServer         |                   | -2147408896         |                 | trusted_root |
| Zscaler Root CA                                                  | 000000000000000000                 | admin        | sslServer         |                   | -2147409654         |                 | trusted_root |
| Zscaler Root CA                                                  | 000000000000000000                 | admin        | sslServer         |                   | -2147408896         |                 | trusted_root |
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+---------------------+-----------------+--------------+

I unfortunately also saw the entires without a common name and I'm not sure what those are about, but they are also omitted with the latest changes.

I also never saw any values for trust_policy_data and trust_key_usage either, but I think this one isn't as scary because I also don't see those in the trust settings array returned by /usr/bin/security dump-trust-settings.

@Micah-Kolide Micah-Kolide requested a review from zwass December 11, 2025 16:41
Copy link
Member

@zwass zwass left a comment

Choose a reason for hiding this comment

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

Nice improvements! My last request is to return the error as a string. This code seems to work and correspond with what the CLI command returns:

          // Log the trust_allowed_error as a string (convert with
          // SecCopyErrorMessageString)
          uint32_t trust_allowed_error_value;
          CFNumberGetValue((CFNumberRef)trust_allowed_error,
                           kCFNumberSInt32Type,
                           &trust_allowed_error_value);
          CFStringRef error =
              SecCopyErrorMessageString(trust_allowed_error_value, nil);
          LOG(ERROR) << "Trust allowed error: " << stringFromCFString(error)
                     << " name:: " << r["common_name"]
                     << " policy:: " << r["trust_policy_name"];
          CFRelease(error);

@Micah-Kolide Micah-Kolide requested a review from zwass December 11, 2025 18:54
@Micah-Kolide
Copy link
Contributor Author

Micah-Kolide commented Dec 11, 2025

Nice improvements! My last request is to return the error as a string. This code seems to work and correspond with what the CLI command returns:

          // Log the trust_allowed_error as a string (convert with
          // SecCopyErrorMessageString)
          uint32_t trust_allowed_error_value;
          CFNumberGetValue((CFNumberRef)trust_allowed_error,
                           kCFNumberSInt32Type,
                           &trust_allowed_error_value);
          CFStringRef error =
              SecCopyErrorMessageString(trust_allowed_error_value, nil);
          LOG(ERROR) << "Trust allowed error: " << stringFromCFString(error)
                     << " name:: " << r["common_name"]
                     << " policy:: " << r["trust_policy_name"];
          CFRelease(error);

Thank you! Here are the results with the trust_allowed_error being a string:

+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+-------------------------+-----------------+--------------+
| common_name                                                      | serial                             | trust_domain | trust_policy_name | trust_policy_data | trust_allowed_error     | trust_key_usage | trust_result |
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+-------------------------+-----------------+--------------+
| Charles Proxy CA (13 Sep 2024, )        | 000000000000                       | admin        | sslServer         |                   | CSSMERR_TP_CERT_EXPIRED |                 | trusted_root |
| Charles Proxy CA (13 Sep 2024, )        | 000000000000                       | admin        | sslServer         |                   | Host name mismatch      |                 | trusted_root |
| Zscaler Root CA                                                  | 000000000000000000                 | admin        | sslServer         |                   | CSSMERR_TP_CERT_EXPIRED |                 | trusted_root |
| Zscaler Root CA                                                  | 000000000000000000                 | admin        | sslServer         |                   | Host name mismatch      |                 | trusted_root |
+------------------------------------------------------------------+------------------------------------+--------------+-------------------+-------------------+-------------------------+-----------------+--------------+

Copy link
Member

@zwass zwass left a comment

Choose a reason for hiding this comment

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

Hopefully last changes 🤞

@Micah-Kolide Micah-Kolide requested a review from zwass December 11, 2025 22:14
Copy link
Member

@zwass zwass left a comment

Choose a reason for hiding this comment

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

Awesome, thank you for the persistence!

@zwass zwass merged commit 95fa419 into osquery:master Dec 15, 2025
21 checks passed
@Micah-Kolide
Copy link
Contributor Author

Awesome, thank you for the persistence!

Of course! Thank you for the very helpful and patient reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants