diff --git a/AADDeviceTrust.Client/Private/Get-AzureADDeviceID.ps1 b/AADDeviceTrust.Client/Private/Get-AzureADDeviceID.ps1 index 5c9c233..9a5e071 100644 --- a/AADDeviceTrust.Client/Private/Get-AzureADDeviceID.ps1 +++ b/AADDeviceTrust.Client/Private/Get-AzureADDeviceID.ps1 @@ -10,10 +10,13 @@ function Get-AzureADDeviceID { Author: Nickolaj Andersen Contact: @NickolajA Created: 2021-05-26 - Updated: 2021-05-26 + Updated: 2023-06-20 Version history: 1.0.0 - (2021-05-26) Function created + 1.0.1 - (2022-10-20) @AzureToTheMax - Fixed issue pertaining to Cloud PCs (Windows 365) devices ability to locate their AzureADDeviceID. + 1.0.2 - (2023-06-20) @AzureToTheMax - Fixed issue pertaining to Cloud PCs (Windows 365) devices where the reported AzureADDeviceID was in all capitals, breaking signature creation. + #> Process { # Define Cloud Domain Join information registry path @@ -27,10 +30,34 @@ function Get-AzureADDeviceID { if ($AzureADJoinCertificate -ne $null) { # Determine the device identifier from the subject name $AzureADDeviceID = ($AzureADJoinCertificate | Select-Object -ExpandProperty "Subject") -replace "CN=", "" + # Convert upper to lowercase. + $AzureADDeviceID = "$($AzureADDeviceID)".ToLower() # Handle return value return $AzureADDeviceID + + } else { + + #If no certificate was found, locate it by Common Name instead of Thumbprint. This is likely a CPC or similar. + $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Subject -like "CN=($AzureADJoinInfoThumbprint)" } + + if ($AzureADJoinCertificate -ne $null){ + # Cert is now found, extract Device ID from Common Name + $AzureADDeviceID = ($AzureADJoinCertificate | Select-Object -ExpandProperty "Subject") -replace "CN=", "" + # Convert upper to lowercase. + $AzureADDeviceID = "$($AzureADDeviceID)".ToLower() + # Handle return value + return $AzureADDeviceID + + } else { + # Last ditch effort, try and use the ThumbPrint (reg key) itself. + $AzureADDeviceID=$AzureADJoinInfoThumbprint + # Convert upper to lowercase. + $AzureADDeviceID = "$($AzureADDeviceID)".ToLower() + return $AzureADDeviceID + + } } } } -} \ No newline at end of file +} diff --git a/AADDeviceTrust.Client/Private/Get-AzureADRegistrationCertificateThumbprint.ps1 b/AADDeviceTrust.Client/Private/Get-AzureADRegistrationCertificateThumbprint.ps1 index 197347a..8da2695 100644 --- a/AADDeviceTrust.Client/Private/Get-AzureADRegistrationCertificateThumbprint.ps1 +++ b/AADDeviceTrust.Client/Private/Get-AzureADRegistrationCertificateThumbprint.ps1 @@ -14,6 +14,7 @@ function Get-AzureADRegistrationCertificateThumbprint { Version history: 1.0.0 - (2021-06-03) Function created + 1.0.1 - (2023-05-10) @AzureToTheMax Updated for Cloud PCs which don't have their thumbprint as their JoinInfo key name. #> Process { # Define Cloud Domain Join information registry path @@ -21,8 +22,27 @@ function Get-AzureADRegistrationCertificateThumbprint { # Retrieve the child key name that is the thumbprint of the machine certificate containing the device identifier guid $AzureADJoinInfoThumbprint = Get-ChildItem -Path $AzureADJoinInfoRegistryKeyPath | Select-Object -ExpandProperty "PSChildName" + # Check for a cert matching that thumbprint + $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $AzureADJoinInfoThumbprint } + + if($AzureADJoinCertificate -ne $null){ + # if a matching cert was found tied to that reg key (thumbprint) value, then that is the thumbprint and it can be returned. + $AzureADThumbprint = $AzureADJoinInfoThumbprint + + # Handle return value + return $AzureADThumbprint + + } else { + + # If a cert was not found, that reg key was not the thumbprint but can be used to locate the cert as it is likely the Azure ID which is in the certs common name. + $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Subject -like "CN=$($AzureADJoinInfoThumbprint)" } + + #Pull thumbprint from cert + $AzureADThumbprint = $AzureADJoinCertificate.Thumbprint + + # Handle return value + return $AzureADThumbprint + } - # Handle return value - return $AzureADJoinInfoThumbprint } -} \ No newline at end of file +} diff --git a/AADDeviceTrust.Client/Private/Get-PublicKeyBytesEncodedString.ps1 b/AADDeviceTrust.Client/Private/Get-PublicKeyBytesEncodedString.ps1 index 8e9c1d7..c237d1d 100644 --- a/AADDeviceTrust.Client/Private/Get-PublicKeyBytesEncodedString.ps1 +++ b/AADDeviceTrust.Client/Private/Get-PublicKeyBytesEncodedString.ps1 @@ -14,10 +14,11 @@ function Get-PublicKeyBytesEncodedString { Author: Nickolaj Andersen / Thomas Kurth Contact: @NickolajA Created: 2021-06-07 - Updated: 2021-06-07 + Updated: 2023-05-10 Version history: 1.0.0 - (2021-06-07) Function created + 1.0.1 - (2023-05-10) @AzureToTheMax - Updated to use X509 for the full public key with extended properties in the PEM format Credits to Thomas Kurth for sharing his original C# code. #> @@ -27,14 +28,19 @@ function Get-PublicKeyBytesEncodedString { [string]$Thumbprint ) Process { + # Determine the certificate based on thumbprint input $Certificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $Thumbprint } if ($Certificate -ne $null) { - # Get the public key bytes - [byte[]]$PublicKeyBytes = $Certificate.GetPublicKey() + # Bring the cert into a X509 object + $X509 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New($Certificate) + #Set the type of export to perform + $type = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert + #Export the public cert + $PublicKeyBytes = $X509.Export($type, "") - # Handle return value + # Handle return value - convert to Base64 return [System.Convert]::ToBase64String($PublicKeyBytes) } } -} \ No newline at end of file +} diff --git a/AADDeviceTrust.Client/Public/New-AADDeviceTrustBody.ps1 b/AADDeviceTrust.Client/Public/New-AADDeviceTrustBody.ps1 index f5e7fe6..aecfe5e 100644 --- a/AADDeviceTrust.Client/Public/New-AADDeviceTrustBody.ps1 +++ b/AADDeviceTrust.Client/Public/New-AADDeviceTrustBody.ps1 @@ -1,10 +1,10 @@ function New-AADDeviceTrustBody { <# .SYNOPSIS - Construct the body with the elements for a sucessful device trust validation required by a Function App that's leveraging the AADDeviceTrust.FunctionApp module. + Construct the body with the elements for a successful device trust validation required by a Function App that's leveraging the AADDeviceTrust.FunctionApp module. .DESCRIPTION - Construct the body with the elements for a sucessful device trust validation required by a Function App that's leveraging the AADDeviceTrust.FunctionApp module. + Construct the body with the elements for a successful device trust validation required by a Function App that's leveraging the AADDeviceTrust.FunctionApp module. .EXAMPLE .\New-AADDeviceTrustBody.ps1 @@ -13,16 +13,18 @@ function New-AADDeviceTrustBody { Author: Nickolaj Andersen Contact: @NickolajA Created: 2022-03-14 - Updated: 2022-03-14 + Updated: 2023-05-14 Version history: 1.0.0 - (2022-03-14) Script created + 1.0.1 - (2023-05-10) @AzureToTheMax - Updated to no longer use Thumbprint field, no redundant. + 1.0.2 - (2023-05-14) @AzureToTheMax - Updating to pull the Azure AD Device ID from the certificate itself. #> [CmdletBinding(SupportsShouldProcess = $true)] param() Process { # Retrieve required data for building the request body - $AzureADDeviceID = Get-AzureADDeviceID + $AzureADDeviceID = Get-AzureADDeviceID # Still needed to form the signature. $CertificateThumbprint = Get-AzureADRegistrationCertificateThumbprint $Signature = New-RSACertificateSignature -Content $AzureADDeviceID -Thumbprint $CertificateThumbprint $PublicKeyBytesEncoded = Get-PublicKeyBytesEncodedString -Thumbprint $CertificateThumbprint @@ -30,13 +32,13 @@ function New-AADDeviceTrustBody { # Construct client-side request header $BodyTable = [ordered]@{ DeviceName = $env:COMPUTERNAME - DeviceID = $AzureADDeviceID + #DeviceID = $AzureADDeviceID - Will be pulled from the key. Signature = $Signature - Thumbprint = $CertificateThumbprint + #Thumbprint = $CertificateThumbprint - Will be pulled from the key. PublicKey = $PublicKeyBytesEncoded } # Handle return value return $BodyTable } -} \ No newline at end of file +} diff --git a/AADDeviceTrust.FunctionApp/AADDeviceTrust.FunctionApp.psd1 b/AADDeviceTrust.FunctionApp/AADDeviceTrust.FunctionApp.psd1 index 9f49e63..59ccf21 100644 --- a/AADDeviceTrust.FunctionApp/AADDeviceTrust.FunctionApp.psd1 +++ b/AADDeviceTrust.FunctionApp/AADDeviceTrust.FunctionApp.psd1 @@ -42,7 +42,8 @@ "Get-AzureADDeviceRecord", "New-HashString", "Test-AzureADDeviceAlternativeSecurityIds", - "Test-Encryption" + "Test-Encryption", + "Get-AzureADDeviceIDFromCertificate" ) # Variables to export from this module @@ -75,4 +76,4 @@ } - \ No newline at end of file + diff --git a/AADDeviceTrust.FunctionApp/Public/Get-AzureADDeviceIDFromCertificate.ps1 b/AADDeviceTrust.FunctionApp/Public/Get-AzureADDeviceIDFromCertificate.ps1 new file mode 100644 index 0000000..72eacee --- /dev/null +++ b/AADDeviceTrust.FunctionApp/Public/Get-AzureADDeviceIDFromCertificate.ps1 @@ -0,0 +1,36 @@ +function Get-AzureADDeviceIDFromCertificate { + <# + .SYNOPSIS + Used to pull the Azure Device ID from the provided Base64 certificate. + + .DESCRIPTION + Used by the function app to pull the Azure Device ID from the provided Base64 certificate. + + .NOTES + Author: Maxton Allen + Contact: @AzureToTheMax + Created: 2023-05-14 + Updated: 2023-05-14 + + Version history: + 1.0.0 - (2023-05-14) created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Specify a Base64 encoded value for which an Azure Device ID will be extracted.")] + [ValidateNotNullOrEmpty()] + [string]$Value + ) + Process { + # Convert Value (cert) passed back to X502 Object + $X502 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New([System.Convert]::FromBase64String($Value)) + + # Get the Subject (issued to) + $Subject = $X502.Subject + + # Remove the leading "CN=" + $SubjectTrimed = $Subject.TrimStart("CN=") + + # Handle return + Return $SubjectTrimed + } +} diff --git a/AADDeviceTrust.FunctionApp/Public/New-HashString.ps1 b/AADDeviceTrust.FunctionApp/Public/New-HashString.ps1 index c73d2df..39d4077 100644 --- a/AADDeviceTrust.FunctionApp/Public/New-HashString.ps1 +++ b/AADDeviceTrust.FunctionApp/Public/New-HashString.ps1 @@ -17,6 +17,9 @@ function New-HashString { Version history: 1.0.0 - (2021-08-23) Function created + + #AzureToTheMax was here - this function does not appear to be used anywhere? If it is, it may need to be updated to accept a full PEM and use the X502 class like the others. + #> param( [parameter(Mandatory = $true, HelpMessage = "Specify a Base64 encoded value for which a hash will be computed.")] @@ -39,4 +42,4 @@ function New-HashString { # Handle return value return $ComputedHashString } -} \ No newline at end of file +} diff --git a/AADDeviceTrust.FunctionApp/Public/Test-AzureADDeviceAlternativeSecurityIds.ps1 b/AADDeviceTrust.FunctionApp/Public/Test-AzureADDeviceAlternativeSecurityIds.ps1 index 99630bc..ebc0154 100644 --- a/AADDeviceTrust.FunctionApp/Public/Test-AzureADDeviceAlternativeSecurityIds.ps1 +++ b/AADDeviceTrust.FunctionApp/Public/Test-AzureADDeviceAlternativeSecurityIds.ps1 @@ -19,10 +19,14 @@ function Test-AzureADDeviceAlternativeSecurityIds { Author: Nickolaj Andersen Contact: @NickolajA Created: 2021-06-07 - Updated: 2021-06-07 + Updated: 2023-05-10 Version history: 1.0.0 - (2021-06-07) Function created + 1.0.1 - (2023-05-10) @AzureToTheMax + 1. Updated Thumbprint compare to use actual PEM cert via X502 class rather than simply a passed and separate thumbprint value. + 2. Updated Hash compare to use full PEM cert via the X502 class, pull out just the public key data, and compare from that like before. + #> param( [parameter(Mandatory = $true, HelpMessage = "Specify the alternativeSecurityIds.Key property from an Azure AD device record.")] @@ -44,8 +48,13 @@ function Test-AzureADDeviceAlternativeSecurityIds { switch ($Type) { "Thumbprint" { + Write-Output "Using new X502 Thumbprint compare" + + # Convert Value (cert) passed back to X502 Object + $X502 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New([System.Convert]::FromBase64String($Value)) + # Validate match - if ($Value -match $AzureADDeviceAlternativeSecurityIds.Thumbprint) { + if ($X502.thumbprint -match $AzureADDeviceAlternativeSecurityIds.Thumbprint) { return $true } else { @@ -53,8 +62,16 @@ function Test-AzureADDeviceAlternativeSecurityIds { } } "Hash" { + Write-Output "Using new X502 hash compare" + + # Convert Value (cert) passed back to X502 Object + $X502 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New([System.Convert]::FromBase64String($Value)) + + # Pull out just the public key, removing extended values + $X502Pub = [System.Convert]::ToBase64String($X502.PublicKey.EncodedKeyValue.rawData) + # Convert from Base64 string to byte array - $DecodedBytes = [System.Convert]::FromBase64String($Value) + $DecodedBytes = [System.Convert]::FromBase64String($X502Pub) # Construct a new SHA256Managed object to be used when computing the hash $SHA256Managed = New-Object -TypeName "System.Security.Cryptography.SHA256Managed" @@ -75,4 +92,4 @@ function Test-AzureADDeviceAlternativeSecurityIds { } } } -} \ No newline at end of file +} diff --git a/AADDeviceTrust.FunctionApp/Public/Test-Encryption.ps1 b/AADDeviceTrust.FunctionApp/Public/Test-Encryption.ps1 index 881a808..1427160 100644 --- a/AADDeviceTrust.FunctionApp/Public/Test-Encryption.ps1 +++ b/AADDeviceTrust.FunctionApp/Public/Test-Encryption.ps1 @@ -19,10 +19,11 @@ function Test-Encryption { Author: Nickolaj Andersen / Thomas Kurth Contact: @NickolajA Created: 2021-06-07 - Updated: 2021-06-07 + Updated: 2023-05-10 Version history: 1.0.0 - (2021-06-07) Function created + 1.0.1 - (2023-05-10) @AzureToTheMax - Updated to use full PEM cert via X502, extract the public key, and perform test like before using that. Credits to Thomas Kurth for sharing his original C# code. #> @@ -40,8 +41,16 @@ function Test-Encryption { [string]$Content ) Process { - # Convert from Base64 string to byte array - $PublicKeyBytes = [System.Convert]::FromBase64String($PublicKeyEncoded) + + Write-Output "Using new X502 encryption test" + # Convert Value (cert) passed back to X502 Object + $X502 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New([System.Convert]::FromBase64String($PublicKeyEncoded)) + + # Pull out just the public key, removing extended values + $X502Pub = [System.Convert]::ToBase64String($X502.PublicKey.EncodedKeyValue.rawData) + + # Convert encoded public key from Base64 string to byte array + $PublicKeyBytes = [System.Convert]::FromBase64String($X502Pub) # Convert signature from Base64 string [byte[]]$Signature = [System.Convert]::FromBase64String($Signature) @@ -74,4 +83,4 @@ function Test-Encryption { # Verify the signature with the computed hash of the content using the public key $PublicKey.VerifyHash($ComputedHash, $Signature, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) } -} \ No newline at end of file +} diff --git a/README.md b/README.md index c45ad0e..d695368 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,56 @@ # Overview -When building a Function App API in Azure that accepts incoming HTTP requests from an Azure AD joined devices, being able to validate such a request is only coming from a trusted device in a given Azure AD tenant adds extensive security to the API. By default, the Function App can be configured to only accept incoming requests with a valid client certificate, which is a good security practice. Although, there's also another option to enhance the security of a Function App in terms of validating the incoming request, using the certificate enrolled to the device when it first registered itself with Azure AD. +When building a Function App API in Azure that accepts incoming HTTP requests from Azure AD joined devices, being able to validate such a request is only coming from a trusted device in a given Azure AD tenant adds extensive security to the API. By default, the Function App can be configured to only accept incoming requests with a valid client certificate, which is a good security practice. Although, there's also another option to enhance the security of a Function App in terms of validating the incoming request, using the certificate enrolled to the device when it first registered itself with Azure AD. This module performs the device trust validation and can be embedded in most Function Apps where enhanced request validation is required. # How the trusted device validation works -Every Azure AD joined or hybrid Azure AD joined device has a computer certificate that was enrolled when registering the device to Azure AD. This device specific computer certificate's public and private keys are available locally on the device, while the public key is known to Azure AD. The device trust validation functionality occurs in the following scenarios: +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"](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrj/f900e812-8f1c-4345-9ab0-b91111068651) 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 signature hash using the private key of the computer certificate is calculated and sent encoded as a Base64 string to the Function App including the Azure AD device identifier (the common name of the computer certificate), the public key as a byte array encoded as a Base64 string together with the computer certificate thumbprint. These data strings are sent all together as parameter input when calling the Function App API +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 -To be added... +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** + +1. 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. + +2. 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. + +3. 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. + +4. 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. + # How to use AADDeviceTrust.Client module in a client-side script Ensure the AADDeviceTrust.Client module is installed on the device prior to running the sample code below. Use the `Test-AzureADDeviceRegistration` function to ensure the device where the code is running on fulfills the device registration requirements. Then use the `New-AADDeviceTrustBody` function to automatically generate a hash-table object containing the gathered data required for the body of the request. Finally, use built-in `Invoke-RestMethod` cmdlet to invoke the request against the Function App, passing the gathered data to be validated by the Function App, if the request comes from a trusted device. @@ -27,7 +61,9 @@ if (Test-AzureADDeviceRegistration -eq $true) { $BodyTable = New-AADDeviceTrustBody # Extend body table with custom data to be processed by Function App - # ... + $BodyTable.Add("Key", "Value") # Example only. This format works for most situations. + $BodyTable.Add("Key2", "$($Value)") # Example only. This format works for most situations involving simple variables. + $BodyTable.Add("EmbededPSObject", $EmbededPSObject) # Other situations such as embedding a PSObject/JSON may need alternate formatting such as this. # Send log data to Function App $URI = "https://.azurewebsites.net/api/?code=" @@ -40,6 +76,7 @@ else { For a full sample of the client-side script, explore the code in \Samples\ClientSide.ps1 in this repo. + # How to use AADDeviceTrust.FunctionApp module in a Function App Enable the module to be installed as a managed dependency by editing your requirements.psd1 file of the Function App, e.g. as shown below: @@ -48,7 +85,14 @@ Enable the module to be installed as a managed dependency by editing your requir 'AADDeviceTrust.FunctionApp' = '1.*' } ``` +You will also need to grant your Function App Device.Read.All Graph permissions using it's managed identity. This is done such that the Function App can pull the devices Azure AD records. -Another option would also be to clone this module from GitHub and include it in the modules folder of your Function App, to embedd it directly and not have a dependency to PSGallery. +Another option would also be to clone this module from GitHub and include it in the modules folder of your Function App, to embed it directly and not have a dependency to PSGallery. For a full sample Function App function, explore the code in \Samples\FunctionApp-MSI.ps1 in this repo. + + +# What certificate is sent to the Function App? +If you are curious to know and see the Certificate sent to the Function App in a friendly format, you can use the \Samples\Cert-Exporter-Sample.ps1 to generate a .CER file. This is done by taking the same Base64 content and slightly re-arranging it into the PEM format. The Base64 itself simply needs to be broken at every 64th character, then the appropriete cert start and end headers are added to the top and bottom. + +This will allow you to visually see the cert, visually confirm the private key is not attached, and confirm the cert is issues to your devices Azure AD ID. The file is generated in the run location of the script and will be named Cert.cer. diff --git a/Samples/Cert-Exporter-Sample.ps1 b/Samples/Cert-Exporter-Sample.ps1 new file mode 100644 index 0000000..b227e33 --- /dev/null +++ b/Samples/Cert-Exporter-Sample.ps1 @@ -0,0 +1,103 @@ + <# + .SYNOPSIS + Export the CERT to be uploaded in .cer/PEM format to the running directory. + + .DESCRIPTION + Export the CERT to be uploaded in .cer/PEM format to the running directory. Used for testing and validaiton. + Provides visual confirmation that the private key is not part of the cert. + Provides visual confirmation that the CN is the Azure AD Device ID. + + .NOTES + Author: Maxton Allen + Contact: @AzureToTheMax + Created: 2023-05-14 + Updated: 2023-05-14 + + Version history: + 1.0.0 - (2023-05-14) created + #> + + +function Get-AzureADRegistrationCertificateThumbprint { + <# + .SYNOPSIS + Get the thumbprint of the certificate used for Azure AD device registration. + + .DESCRIPTION + Get the thumbprint of the certificate used for Azure AD device registration. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-06-03 + Updated: 2021-06-03 + + Version history: + 1.0.0 - (2021-06-03) Function created + 1.0.1 - (2023-05-10) @AzureToTheMax Updated for Cloud PCs which don't have their thumbprint as their JoinInfo key name. + #> + Process { + # Define Cloud Domain Join information registry path + $AzureADJoinInfoRegistryKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo" + + # Retrieve the child key name that is the thumbprint of the machine certificate containing the device identifier guid + $AzureADJoinInfoThumbprint = Get-ChildItem -Path $AzureADJoinInfoRegistryKeyPath | Select-Object -ExpandProperty "PSChildName" + # Check for a cert matching that thumbprint + $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $AzureADJoinInfoThumbprint } + + if($AzureADJoinCertificate -ne $null){ + # if a matching cert was found tied to that reg key (thumbprint) value, then that is the thumbprint and it can be returned. + $AzureADThumbprint = $AzureADJoinInfoThumbprint + + # Handle return value + return $AzureADThumbprint + + } else { + + # If a cert was not found, that reg key was not the thumbprint but can be used to locate the cert as it is likely the Azure ID which is in the certs common name. + $AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Subject -like "CN=$($AzureADJoinInfoThumbprint)" } + + #Pull thumbprint from cert + $AzureADThumbprint = $AzureADJoinCertificate.Thumbprint + + # Handle return value + return $AzureADThumbprint + } + + } +} + +$thumbprint = Get-AzureADRegistrationCertificateThumbprint +#Get the cert as base64 + $Certificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $Thumbprint } + if ($Certificate -ne $null) { + # Bring the cert into a X509 object + $X509 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New($Certificate) + #Set the type of export to perform + $type = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert + #Export the public cert + $PublicKeyBytes = $X509.Export($type, "") + + # Handle return value - convert to Base64 + $PublicKeyEncoded = [System.Convert]::ToBase64String($PublicKeyBytes) + } + +#Alter formatting + #Break it every 64 characters to make it into the CER format + $PublicKeyBroken = $PublicKeyEncoded | + ForEach-Object { + $line = $_ + + for ($i = 0; $i -lt $line.Length; $i += 64) + { + $length = [Math]::Min(64, $line.Length - $i) + $line.SubString($i, $length) + } + } + +#Set cer content to current working directory +Set-Content ".\cert.cer" "-----BEGIN CERTIFICATE----- +$PublicKeyBroken +-----END CERTIFICATE-----" + +#Your cert should now be in the working directory as cert.cer and can be opened natively by Windows for inspection. \ No newline at end of file diff --git a/Samples/FunctionApp-MSI.ps1 b/Samples/FunctionApp-MSI.ps1 index e2534e3..67b38b7 100644 --- a/Samples/FunctionApp-MSI.ps1 +++ b/Samples/FunctionApp-MSI.ps1 @@ -1,132 +1,144 @@ -using namespace System.Net - -# Input bindings are passed in via param block. -param( - [Parameter(Mandatory = $true)] - $Request, - - [Parameter(Mandatory = $false)] - $TriggerMetadata -) - -# Functions -function Get-AuthToken { <# .SYNOPSIS - Retrieve an access token for the Managed System Identity. - - .DESCRIPTION - Retrieve an access token for the Managed System Identity. + HTTP Function App sample .NOTES - Author: Nickolaj Andersen - Contact: @NickolajA - Created: 2021-06-07 - Updated: 2021-06-07 + Author: Nickolaj Andersen, Maxton Allen + Contact: @NickolajA, @AzureToTheMax + Created: 2022-01-25 + Updated: 2023-05-14 Version history: - 1.0.0 - (2021-06-07) Function created - #> - Process { - # Get Managed Service Identity details from the Azure Functions application settings - $MSIEndpoint = $env:MSI_ENDPOINT - $MSISecret = $env:MSI_SECRET + 1.0.0 - 2022-01-25 created + 1.0.1 - 2023-05-11 Updated to use X509 class + 1.0.2 - 2023-05-14 Updated to pull Azure AD Device ID from the cert - # Define the required URI and token request params - $APIVersion = "2017-09-01" - $ResourceURI = "https://graph.microsoft.com" - $AuthURI = $MSIEndpoint + "?resource=$($ResourceURI)&api-version=$($APIVersion)" + + #> - # Call resource URI to retrieve access token as Managed Service Identity - $Response = Invoke-RestMethod -Uri $AuthURI -Method "Get" -Headers @{ "Secret" = "$($MSISecret)" } + using namespace System.Net - # Construct authentication header to be returned from function - $AuthenticationHeader = @{ - "Authorization" = "Bearer $($Response.access_token)" - "ExpiresOn" = $Response.expires_on + # Input bindings are passed in via param block. + param( + [Parameter(Mandatory = $true)] + $Request, + + [Parameter(Mandatory = $false)] + $TriggerMetadata + ) + + # Functions + + function Get-SelfGraphAuthToken { + <# + .SYNOPSIS + Use the permissions granted to the Function App itself to obtain a Graph token for running Graph queries. + Returns a formated header for use with the original code. + + .NOTES + Author: Nickolaj Andersen, Maxton Allen + Contact: @NickolajA, @AzureToTheMax + Created: 2021-06-07 + Updated: 2023-02-17 + + Version history: + 1.0.0 - 2021-06-07 Function created + 1.0.1 - 2023-02-17 @AzureToTheMax - Updated to API Version 2019-08-01 from 2017-09-01 + #> + Process { + + $resourceURI = "https://graph.microsoft.com" + $tokenAuthURI = $env:IDENTITY_ENDPOINT + "?resource=$resourceURI&api-version=2019-08-01" + $tokenResponse = Invoke-RestMethod -Method Get -Headers @{"X-IDENTITY-HEADER"="$env:IDENTITY_HEADER"} -Uri $tokenAuthURI + + + $AuthenticationHeader = @{ + "Authorization" = "Bearer $($tokenResponse.access_token)" + "ExpiresOn" = $tokenResponse.expires_on + } + return $AuthenticationHeader } - - # Handle return value - return $AuthenticationHeader - } -} - -# Retrieve authentication token -$AuthToken = Get-AuthToken - -# Initate variables -$StatusCode = [HttpStatusCode]::OK -$Body = [string]::Empty - -# Assign incoming request properties to variables -$DeviceName = $Request.Body.DeviceName -$DeviceID = $Request.Body.DeviceID -$Signature = $Request.Body.Signature -$Thumbprint = $Request.Body.Thumbprint -$PublicKey = $Request.Body.PublicKey - -# Initiate request handling -Write-Output -InputObject "Initiating request handling for device named as '$($DeviceName)' with identifier: $($DeviceID)" - -# Retrieve Azure AD device record based on DeviceID property from incoming request body -$AzureADDeviceRecord = Get-AzureADDeviceRecord -DeviceID $DeviceID -AuthToken $AuthToken -if ($AzureADDeviceRecord -ne $null) { - Write-Output -InputObject "Found trusted Azure AD device record with object identifier: $($AzureADDeviceRecord.id)" - - # Validate thumbprint from input request with Azure AD device record's alternativeSecurityIds details - if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Thumbprint" -Value $Thumbprint) { - Write-Output -InputObject "Successfully validated certificate thumbprint from inbound request" - - # Validate public key hash from input request with Azure AD device record's alternativeSecurityIds details - if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Hash" -Value $PublicKey) { - Write-Output -InputObject "Successfully validated certificate SHA256 hash value from inbound request" - - $EncryptionVerification = Test-Encryption -PublicKeyEncoded $PublicKey -Signature $Signature -Content $AzureADDeviceRecord.deviceId - if ($EncryptionVerification -eq $true) { - Write-Output -InputObject "Successfully validated inbound request came from a trusted Azure AD device record" - - # Validate that the inbound request came from a trusted device that's not disabled - if ($AzureADDeviceRecord.accountEnabled -eq $true) { - Write-Output -InputObject "Azure AD device record was validated as enabled" - - # - # - # Place your code here, at this stage incoming request has been validated as trusted - # - # + }#end function + + + + # Retrieve authentication token + $AuthToken = Get-SelfGraphAuthToken + + # Initate variables + $StatusCode = [HttpStatusCode]::OK + $Body = [string]::Empty + + # Assign incoming request properties to variables + $DeviceName = $Request.Body.DeviceName + $Signature = $Request.Body.Signature + $PublicKey = $Request.Body.PublicKey + + #Get Device ID from the cert + $DeviceID = Get-AzureADDeviceIDFromCertificate -Value $PublicKey + + # Initiate request handling + Write-Output -InputObject "Initiating request handling for device named as '$($DeviceName)' with cert containing Azure AD identifier: $($DeviceID)" + + # Retrieve Azure AD device record based on DeviceID property from incoming request body + $AzureADDeviceRecord = Get-AzureADDeviceRecord -DeviceID $DeviceID -AuthToken $AuthToken + if ($AzureADDeviceRecord -ne $null) { + Write-Output -InputObject "Found trusted Azure AD device record with object identifier: $($AzureADDeviceRecord.id)" + + # Validate thumbprint from input request with Azure AD device record's alternativeSecurityIds details + if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Thumbprint" -Value $PublicKey) { + Write-Output -InputObject "Successfully validated certificate thumbprint from inbound request" + + # Validate public key hash from input request with Azure AD device record's alternativeSecurityIds details + if (Test-AzureADDeviceAlternativeSecurityIds -AlternativeSecurityIdKey $AzureADDeviceRecord.alternativeSecurityIds.key -Type "Hash" -Value $PublicKey) { + Write-Output -InputObject "Successfully validated certificate SHA256 hash value from inbound request" + + $EncryptionVerification = Test-Encryption -PublicKeyEncoded $PublicKey -Signature $Signature -Content $AzureADDeviceRecord.deviceId + if ($EncryptionVerification -eq $true) { + Write-Output -InputObject "Successfully validated inbound request came from a trusted Azure AD device record" + + # Validate that the inbound request came from a trusted device that's not disabled + if ($AzureADDeviceRecord.accountEnabled -eq $true) { + Write-Output -InputObject "Azure AD device record was validated as enabled" + + # + # + # Place your code here, at this stage incoming request has been validated as trusted + # + # + } + else { + Write-Output -InputObject "Trusted Azure AD device record validation for inbound request failed, record with deviceId '$($DeviceID)' is disabled" + $StatusCode = [HttpStatusCode]::Forbidden + $Body = "Disabled device record" + } } else { - Write-Output -InputObject "Trusted Azure AD device record validation for inbound request failed, record with deviceId '$($DeviceID)' is disabled" + Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate signed content from client" $StatusCode = [HttpStatusCode]::Forbidden - $Body = "Disabled device record" + $Body = "Untrusted request" } } else { - Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate signed content from client" + Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate SHA256 hash value" $StatusCode = [HttpStatusCode]::Forbidden $Body = "Untrusted request" } } else { - Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate SHA256 hash value" + Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate thumbprint" $StatusCode = [HttpStatusCode]::Forbidden $Body = "Untrusted request" } } else { - Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not validate certificate thumbprint" + Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not find device with deviceId: $($DeviceID)" $StatusCode = [HttpStatusCode]::Forbidden $Body = "Untrusted request" } -} -else { - Write-Warning -Message "Trusted Azure AD device record validation for inbound request failed, could not find device with deviceId: $($DeviceID)" - $StatusCode = [HttpStatusCode]::Forbidden - $Body = "Untrusted request" -} - -# Associate values to output bindings by calling 'Push-OutputBinding'. -Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = $Body -}) \ No newline at end of file + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + })