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

Fix regression issue in IClientCertificate caused by SHA256 changes #898

Merged
merged 6 commits into from
Feb 4, 2025

Conversation

Avery-Dunn
Copy link
Collaborator

Fix for the issue described in #863

As part of #840, the internal implementation of the IClientCertificate interface's publicCertificateHash() method was changed to use SHA-256, and a new publicCertificateHashSha1() method was added to the internal implementation to handle scenarios that still needed SHA-1.

However, this meant that anyone who implemented their own IClientCredential instead of using our ClientCredentialFactory would have a publicCertificateHash() method that still used SHA-1, causing issues because our library expected that method to produce a SHA-256 hash.

This PR solves that issue by adding a new publicCertificateHash256 method to the public interface to handle SHA-256 scenarios, and once again has publicCertificateHash handling the SHA-1 scenarios.

The new interface method is given a default implementation so that it doesn't break any customer's implementations, and if it's not implemented then the library falls back to publicCertificateHash and SHA-1. Anyone who was always using our ClientCredentialFactory should not encounter any issues from either the original changes or the ones in this PR.

@Avery-Dunn Avery-Dunn requested a review from a team as a code owner January 10, 2025 21:21
@@ -36,4 +41,35 @@ void testGetClient() {
final ClientCertificate kc = ClientCertificate.create(key, null);
assertNotNull(kc);
}

@Test
void testIClientCertificateInterface_Sha256AndSha1() throws NoSuchAlgorithmException, CertificateException {
Copy link
Member

Choose a reason for hiding this comment

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

Please create a better E2E (not necessarily integration) test that uses MSAL public API. To assert, you can intercept the HTTP call to the /token endpoint, get the client_assertion parameter and observe that it has x5t#s256 which is correctly computed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the latest commit I added a more thorough test: everything up until the call to the token endpoint is real, and then the mocked response will only be successful if 'x5t#s256' is one of the headers in the assertion.

@@ -97,4 +105,34 @@ static HttpResponse expectedResponse(int statusCode, String response) {

return httpResponse;
}

static void setPrivateKeyAndCert() {
Copy link
Member

Choose a reason for hiding this comment

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

Can you use a static certificate instead? Like in MISE? That way you can assert on the the well known hash too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The variables are already static, or are you saying to just create one locally and add it to the repo?

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

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

Insufficient testing.

…tion-library-for-java into avdunn/sha256-fix

# Conflicts:
#	msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java
@Avery-Dunn Avery-Dunn requested a review from bgavrilMS January 22, 2025 18:30
Comment on lines 104 to 108
if (Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS) {
clientAuthentication = buildValidClientCertificateAuthoritySha1();
//When this was added, ADFS did not support SHA256 hashes for client certificates
clientAuthentication = buildValidClientCertificateAuthority(true);
} else {
clientAuthentication = buildValidClientCertificateAuthority();
clientAuthentication = buildValidClientCertificateAuthority(false);
Copy link

@Robbie-Microsoft Robbie-Microsoft Jan 22, 2025

Choose a reason for hiding this comment

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

You could eliminate the if statement and call buildValidClientCertificateAuthority only once:

const useSha1: boolean = (Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS);
clientAuthentication = buildValidClientCertificateAuthority(useSha1)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, in the latest commit I moved this logic down into the buildValidClientCertificateAuthority method. Now that method does the ADFS authority check itself, with a comment explaining why it's done.

false);
return createClientAuthFromClientAssertion(clientAssertion);
}

//The library originally used SHA-1 for thumbprints as other algorithms were not supported server-side,
// and while support for SHA-256 has been added certain flows still only allow SHA-1
Copy link

@Robbie-Microsoft Robbie-Microsoft Jan 22, 2025

Choose a reason for hiding this comment

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

super duper nit: add a , after added. In my mind, it makes sense to indicate a pause to break up the sentence - please feel free to ignore this if you want

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the latest commit I fixed the grammar and adjusted the wording a bit.

builder.x509CertThumbprint(new Base64URL(credential.publicCertificateHashSha1()));
//SHA-256 is preferred, however certain flows still require SHA-1 due to what is supported server-side. If SHA-256
// is not supported or the IClientCredential.publicCertificateHash256() method is not implemented, the library will default to SHA-1.
if (useSha1 || credential.publicCertificateHash256() == null) {

Choose a reason for hiding this comment

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

You're using credential.publicCertificateHash256() twice - once in the if check and once in the else. Can you set it to a const before the if check?

(and add a type, below is typescript)

const publicCertificateHash256: hash | null = credential.publicCertificateHash256();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the latest commit it's now only set once.

@Robbie-Microsoft
Copy link

What is the "default" hash algorithm that you're implementing in this PR? Sha1 or Sha256? Your description states (as I interpret it, based on the last paragraph) that Sha256 should be the default, but your code revolves around checks for if Sha1, then do this, else Sha256 instead of if Sha256, then do this, else Sha1.

@Avery-Dunn Avery-Dunn changed the title Fix regression issue in IClientCredential caused by SHA256 changes Fix regression issue in IClientCertificate caused by SHA256 changes Jan 24, 2025
@Avery-Dunn
Copy link
Collaborator Author

What is the "default" hash algorithm that you're implementing in this PR? Sha1 or Sha256? Your description states (as I interpret it, based on the last paragraph) that Sha256 should be the default, but your code revolves around checks for if Sha1, then do this, else Sha256 instead of if Sha256, then do this, else Sha1.

To clarify, 'default' here refers a default method in an interface. If you add a new method to a public interface, it needs a default implementation or else anything that implements the interface would also need to change to implement the new method (which would be a breaking change): https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

So technically the publicCertificateHash() method that returns a SHA-1 hash can be considered the 'default' hashing method, because the library will work fine even if the new publicCertificateHash256() API is not given a 'real' implementation and just returns null. However, the vast majority of customers will use the library's implementation of the IClientCertificate interface, which uses SHA-256 for everything except ADFS.

(as a bit of background, we found this issue because a customer was using the library's implementation of IClientCertificate.publicCertificateHash() in their own token calls. The library's implementation is package-private but the interface is public, so when that method started producing a SHA-256 hash instead of SHA-1 it was a breaking change for their very niche use case)

@Avery-Dunn Avery-Dunn merged commit 175a4eb into dev Feb 4, 2025
5 checks passed
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.

None yet

3 participants