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

Support for FIPS 140-3 #3324

Merged
merged 5 commits into from
Mar 4, 2025
Merged

Support for FIPS 140-3 #3324

merged 5 commits into from
Mar 4, 2025

Conversation

narrieta
Copy link
Member

@narrieta narrieta commented Feb 27, 2025

When fetching certificates from WireServer, the Agent uses DES_EDE3_CBC. The PFX it receives has a MAC computed using PKCS12KDF. Both are deprecated on FIPS 140-3.

This PR switches to AES128_CBC for communication with the WireServer (a subsequent PR will change it to AES256_CBC) and skips MAC verification when it is not needed.

The changes also include some minor cleanup to remove data structures that are not used.

Fixes #2600

xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert())
certs = Certificates(xml_text, self.logger)
# Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history
for c in certs.summary:
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved this logging to the Certificates class

local_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME)
fileutil.write_file(local_file, xml_text)
try:
pfx_file = self._download_certificates_pfx(wire_client, uri)
Copy link
Member Author

Choose a reason for hiding this comment

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

I need to distinguish between failures in the encryption protocol with the WireServer and failures parsing the PFX, so I split those into separate methods. I also moved the creation of the *.prv and *.crt files from the the Certificates.pem to its own method.

xml_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME)
pfx_file = os.path.join(conf.get_lib_dir(), PFX_FILE_NAME)

for cypher in ["AES128_CBC", "DES_EDE3_CBC"]:
Copy link
Member Author

@narrieta narrieta Feb 27, 2025

Choose a reason for hiding this comment

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

The WireServer and OpenSSL on the VM should both support AES128, but I am keeping Triple DES as a fallback. Depending on telemetry, I may remove the fallback on a subsequent release.


try:
xml_text = wire_client.fetch_config(uri, headers)
except Exception as e:
Copy link
Member Author

@narrieta narrieta Feb 27, 2025

Choose a reason for hiding this comment

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

I am catching a generic Exception to collect telemetry that will help me decide what specific errors should be handled with a retry. For the moment, in the worst case, we are doing a retry that won't help, but this is done on a per-goal-state basis, so the impact is minimal.

Same applies for catching a generic shellutil.CommandError in the code below and in _convert_certificates_pfx_to_pem()

thumbprints[pub] = thumbprint
# Rename crt with thumbprint as the file name
crt = "{0}.crt".format(thumbprint)
v1_cert_list.append({
Copy link
Member Author

Choose a reason for hiding this comment

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

this certificate list was not being used, and most of the properties in each item were not even populated

@@ -85,6 +85,7 @@
"*.p7m",
"*.pem",
"*.prv",
"Certificates.xml",
Copy link
Member Author

Choose a reason for hiding this comment

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

The permissions for those files should be set at creation time. For now, just adding a missing file, will set the permissions on creation on a separate PR.

@@ -365,8 +365,7 @@ def test_fallback_channel_failure(self, patch_put, patch_upload, _):
# ensure host plugin is not set as default
self.assertFalse(wire.HostPluginProtocol.is_default_channel)

@patch("azurelinuxagent.common.event.add_event")
Copy link
Member Author

Choose a reason for hiding this comment

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

I reduced the scope of this mock to the call to upload_status_blob(), otherwise it ends up capturing extra telemetry

@@ -497,12 +497,6 @@ def test_report_event_http_req_should_do_max_retries_on_throttling_error(self, m
client.report_event(self._get_telemetry_events_generator(event_list), flush=True)
self.assertEqual(mock_http_request.call_count, 3)

def test_get_header_for_cert_should_use_triple_des(self, *_):
Copy link
Member Author

Choose a reason for hiding this comment

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

not applicable anymore


def info(self, op, fmt, *args):
self._logger.info(fmt, *args)
add_event(op=op, message=fmt.format(*args), is_success=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

should log_event=False for info events?

Copy link
Member Author

Choose a reason for hiding this comment

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

log_event is used only when is_success == False:

    def add_event(self, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION),
                  message="", log_event=True, flush=False):
        if (not is_success) and log_event:
            _log_event(name, op, message, duration, is_success=is_success)

def decrypt_certificates_p7m(self, p7m_file, trans_prv_file, trans_cert_file, pfx_file):
umask = None
try:
umask = os.umask(0o077)
Copy link
Contributor

Choose a reason for hiding this comment

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

we do this because pfx file should have read and write access for owner only?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, I had to do it this way because I could not find an API to create a file with a specific mode that is backwards compatible all the way to 2.6

return
try:
pem_file = self._convert_certificates_pfx_to_pem(pfx_file)
finally:
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a finally block to line 529 and do the pfx removal there if it exists? It seems possible for umask() to raise Exception when we reset the file mode creation in the finally block of decrypt_certificates_p7m?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, thanks. I think it is the responsibility of _download_certificates_pfx to cleanup when it raises. I'll handle this case there

buf = []
index += 1

cryptutil = CryptUtil(conf.get_openssl_cmd())
Copy link
Contributor

Choose a reason for hiding this comment

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

use self._crypt_util instead?

@narrieta narrieta merged commit 4ad9ba8 into Azure:develop Mar 4, 2025
11 checks passed
@narrieta narrieta deleted the fips branch March 4, 2025 20:55
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.

3 participants