Howdy folks!
I have good news! Great news actually, as I think this will let me start to remove the annoying red security warnings on all my HTTP function apps.
Thanks to the work of the @NickolajA of the MSEndpointMGR team, there is a new authentication method for HTTP Functions which is more secure than previous methods.
To my pleasant surprise, I have wound up involved in the documentation & development of this solution. I am currently (as of 5/16/23) waiting on a few updates to get rolled into the production version at which point I can start updating my existing HTTP Function Apps. Still, I am confident enough in that new version to go ahead and discuss how it works.
The Old Authentication Methodology:
On my Functions I use function level authentication. As such, to connect to the functions at all, an API key needs to be provided in the URL. Unfortunately, that API key is stored in plain text inside the client scripts. Still, this is better to have than not have and will be continued in the new version.
Additionally, as part of the overall request devices would submit a Tenant ID and Azure Device ID to the Function app. The Function App would then verify the reported Tenant ID matches that of the Tenant the Function lives in and, that the Azure Device ID reported is a real and enabled device in that same tenant.
This was again better than nothing but, these values could easily be collected off any machine, even by non-admins. Combine that access/information with the right knowledge and anyone could then talk to the function from any device and likely then make the function potentially action on other devices. For example, if you had a Function App deployed to alter group tags or to upload log analytics data, with the right information someone could make those apps alter other devices or upload logs for other devices.
Documentation:
The full documentation (currently missing Function explanation) will eventually make its way to the main GitHub and I highly suggest you check to see if it is there by the time you read this! If it is, I would use it over this article.
I do have my own documentation and previously mentioned alterations completed and pending review on my fork here which is what I will be using on my Functions and copying below.
The New Process:
Note: This process involves certificates. I will do my best to explain things but this may get confusing if you have no prior experience with them.
The below is simply a copy from my GitHub fork as of 5/16/2023. Again, the latest information will always be on the GitHub repo itself.
Every Azure AD joined or hybrid Azure AD joined device has a computer certificate that was generated when registering the device to Azure AD. This device specific computer certificate’s public and private keys are available locally on the device. When registering the device, a special field called the “alternativeSecurityIds” is added to the Device’s Azure record which contains a “key” field with a value that is a Base64 encoded representation of that same private/public key pairs SHA1 Thumbprint, as well as the entire public keys SHA1 hash.
The device trust validation functionality occurs in the two parts:
- Client-side data gathering
- Function App data validation
Client-Side:
On the client side, a table of information is built which will both serve to carry the data needed to authenticate to our Function App, as well as any other payload required for your specific needs. By default, this table contains…
- The devices name.
- A copy of the computers public certificate in PEM format which has been encoded as a Base64 string for ease of transport.
- And last but not least, a signature generated from the SHA256 hash of the devices Azure ID which was signed using the devices private certificate.
…And again, all of that data will be passed to the Function App along with any other data you add. Details on how to add more fields can be found in the use section.
Note: The signature is not an encrypted form of the SHA256 hash of the devices Azure ID, nor does it contain the hash of the Azure ID at all. It also does not contain the Private key. It is merely a method to validate and authenticate a SHA256 hash, and thus the chunk of data it represents (the Azure ID), when combined with the public certificate. How this comes into play is explained in the next section.
Function App:
When the Function App receives a request, it will start by pulling the various information sent by the client out of the body of the request.
The first thing the Function App does is pull the devices Azure AD ID from the certificate provided. It will then use its Graph permissions* to pull the full Azure AD record for that Azure AD Device ID. As mentioned, this record contains a “alternativeSecurityIds” field with a key value that has a base64 representation of the SHA1 thumbprint and SHA1 hash of the full X.509 public certificate used when the machine originally registered.
*Function App needs Device.Read.All permissions
- The authentication then starts by taking the full PEM X.509 public cert provided in our request and pulling out the SHA1 thumbprint of the certificate. It then confirms that thumbprint matches the SHA1 thumbprint stored in the alternativeSecurityIds/keys field. With this, we can confirm that the public key we have provided in our request is at least related to the public key (or more so private key) originally used when the device registered with Azure.
- Next, we confirm that the SHA1 hash of the entire public key that was provided matches the SHA1 hash of the devices public key that was stored in the alternativeSecurityIds/keys field. At this point, we know the public certificate provided is not just related to the same private certificate but is indeed the exact same public certificate originally made when the device was registered.
- Now that we know our public key is legitimate and not just some random key, we are going to test the signature against the SHA256 hash of the devices Azure ID using that public key. In order to do this, we must take our Base64 encoded Public Key, turn it back into a byte array, and convert that back into a functional RSA key. We then pull the devices Azure AD ID, this time from Azure itself, and again calculate it’s SHA256 hash. We can then use our public key to validate that signature against that hash (the hash of the Azure ID) proving that we must also have the matching private key.
- Lastly, we do a simple check to ensure that the Azure AD Device ID is enabled.
With all this confirmed, we know that…
- The request contained a public certificate issued to a valid Azure Device ID.
- The request contained a public certificate with a thumbprint that matches the thumbprint stored in Azure. Now we know this public cert is at least related to the same private cert.
- The request contained a public certificate with a hash that matches the hash of the original public certificate stored in Azure. Now we know that this is the same public cert originally used to register the device.
- The signature file provided is indeed a signed copy of the devices Azure AD ID which, since we know this is the original public key, we can infer/know the original private key was used to sign it.
- That Azure Device ID in question is Enabled.
At this point, the device is authenticated, and the remainder of the request (your custom code) can begin to process.
What alterations were made?
Currently, the main project uploads your Azure AD ID and thumbprint as two separate values in addition to everything else. This means the thumbprint verified could technically be a different one that the thumbprint of the certificate provided (it would still get caught in the hash check) and, that a different device ID could be provided than what is in the cert (which would also still fail). They are not horrible issues, I just saw a cleaner way to upload the full PEM format cert, rather than public key alone, such that we could pull those values from the cert itself since they are part of the cert.
Sounds great! When can we have it?
Soon, I hope. I am again just waiting on my updates to get rolled into the main project at which point I then will go on an updating frenzy for all my Function Apps.
If you have an urgent need, you can use the functions as they are currently and instead of calling on a dependency, you can simply put each function directly into your primary script. This is how I have mine working in testing currently but, I would greatly prefer not to roll them out that way.
Disclaimer:
The following is the disclaimer that applies to all scripts, functions, one-liners, setup examples, documentation, etc. This disclaimer supersedes any disclaimer included in any script, function, one-liner, article, post, etc.
You running this script/function or following the setup example(s) means you will not blame the author(s) if this breaks your stuff. This script/function/setup-example is provided AS IS without warranty of any kind. Author(s) disclaim all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall author(s) be held liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the script or documentation. Neither this script/function/example/documentation, nor any part of it other than those parts that are explicitly copied from others, may be republished without author(s) express written permission. Author(s) retain the right to alter this disclaimer at any time.
It is entirely up to you and/or your business to understand and evaluate the full direct and indirect consequences of using one of these examples or following this documentation.
The latest version of this disclaimer can be found at: https://azuretothemax.net/disclaimer/
