PRT_Utils.ps1
# Aug 21st 2020 function Register-DeviceToAzureAD { [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [String]$AccessToken, [Parameter(Mandatory=$True)] [String]$DeviceName, [Parameter(Mandatory=$False)] [String]$DeviceType, [Parameter(Mandatory=$False)] [String]$OSVersion, [Parameter(Mandatory=$False)] [Bool]$SharedDevice=$False, [Parameter(Mandatory=$False)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [Parameter(Mandatory=$False)] [String]$DomainName, [Parameter(Mandatory=$False)] [Guid]$TenantId, [Parameter(Mandatory=$False)] [String]$DomainController, [Parameter(Mandatory=$False)] [String]$SID, [Parameter(Mandatory=$False)] [Bool]$RegisterOnly=$false ) Process { # If certificate provided, this is a Hybrid Join if($hybrid = $Certificate -ne $null) { # Load the "user" certificate private key try { $privateKey = Load-PrivateKey -Certificate $Certificate } catch { Write-Error "Could not extract the private key from the given certificate!" return } $deviceId = $certificate.Subject.Split("=")[1] try { $deviceIdGuid = [Guid]$deviceId } catch { Write-Error "The certificate subject is not a valid device id (GUID)!" return } # Create the signature blob $clientIdentity = "$($SID).$((Get-Date).ToUniversalTime().ToString("u"))" $bClientIdentity = [System.Text.Encoding]::ASCII.GetBytes($clientIdentity) $signedBlob = $privateKey.SignData($bClientIdentity, "SHA256") $b64SignedBlob = Convert-ByteArrayToB64 -Bytes $signedBlob } else { # Get the domain and tenant id $at_info = Read-Accesstoken -AccessToken $AccessToken if([string]::IsNullOrEmpty($DomainName)) { if($at_info.upn) { $DomainName = $at_info.upn.Split("@")[1] } else { # Access Token fetched with SAML token so no upn # "unique_name" = "http://<domain>/adfs/services/trust/#" $DomainName = $at_info.unique_name.split("/")[2] $hybridSAML = $true } } $tenantId = [GUID]$at_info.tid $headers=@{"Authorization" = "Bearer $AccessToken"} } # Create a private key $rsa = [System.Security.Cryptography.RSA]::Create(2048) # Initialize the Certificate Signing Request object $CN = "CN=7E980AD9-B86D-4306-9425-9AC066FB014A" $req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1) # Create the signing request $csr = Convert-ByteArrayToB64 -Bytes $req.CreateSigningRequest() # Use the public key as a transport key just to make things easier $transportKey = Convert-ByteArrayToB64 -Bytes $rsa.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::GenericPublicBlob) # Create the request body # JoinType 0 = Azure AD join, transport key = public key # JoinType 4 = Azure AD registered, transport key = public key # JoinType 6 = Azure AD hybrid join, transport key = public key. Hybrid join this way is not supported, there must be an existing device with user cert. $body=@{ "CertificateRequest" = @{ "Type" = "pkcs10" "Data" = $csr } "Attributes" = @{ "ReuseDevice" = "$true" "ReturnClientSid" = "$true" "SharedDevice" = "$SharedDevice" } } if($hybrid) { $body["JoinType"] = 6 # Hybrid Join $body["ServerAdJoinData"] = @{ "TransportKey" = $transportKey "TargetDomain" = $DomainName "DeviceType" = $DeviceType "OSVersion" = $OSVersion "DeviceDisplayName" = $DeviceName "SourceDomainController" = $DomainController "TargetDomainId" = $tenantId.ToString() "ClientIdentity" = @{ "Type" = "sha256signed" "Sid" = $clientIdentity "SignedBlob" = $b64SignedBlob } } } else { if($hybridSAML) { $body["JoinType"] = 6 # Hybrid Join } elseif($RegisterOnly) { $body["JoinType"] = 4 # Register } else { $body["JoinType"] = 0 # Join } $body["TransportKey"] = $transportKey $body["TargetDomain"] = $DomainName $body["DeviceType"] = $DeviceType $body["OSVersion"] = $OSVersion $body["DeviceDisplayName"] = $DeviceName } # Make the enrollment request try { if($hybrid) { $response = Invoke-RestMethod -UseBasicParsing -Method Put -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/$deviceId`?api-version=1.0" -Body $($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8" } else { $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/?api-version=1.0" -Headers $headers -Body $($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8" } } catch { Write-Error $_ return } Write-Debug "RESPONSE: $response" # Get the certificate $binCert = [byte[]] (Convert-B64ToByteArray -B64 $response.Certificate.RawBody) # Create a new x509certificate $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -band [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) # Store the private key to so that it can be exported $cspParameters = [System.Security.Cryptography.CspParameters]::new() $cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" $cspParameters.ProviderType = 24 $cspParameters.KeyContainerName ="AADInternals" # Set the private key $privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters) $privateKey.ImportParameters($rsa.ExportParameters($true)) $cert.PrivateKey = $privateKey # Return $returnValue=@( $cert $response ) return $returnValue } } # Aug 21st 2020 function Sign-JWT { [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [System.Security.Cryptography.RSA]$PrivateKey, [Parameter(Mandatory=$False)] [Byte[]]$Key, [Parameter(Mandatory=$True)] [byte[]]$Data ) Process { if($PrivateKey) { # Sign the JWT (RS256) $signature = $PrivateKey.SignData($Data, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) } else { # Sign the JWT (HS256) $hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList @(,$Key) $signature = $hmac.ComputeHash($Data) } # Return return $signature } } # Aug 22nd 2020 # Parses the JWE and decrypts the session key function Decrypt-PRTSessionKey { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$JWE, [Parameter(Mandatory=$True)] [System.Security.Cryptography.RSA]$PrivateKey ) Process { # Get the encoded key $parts = $JWE.Split(".") $header = [text.encoding]::UTF8.GetString((Convert-B64ToByteArray -B64 $parts[0])) | ConvertFrom-Json $encKey = Convert-B64ToByteArray -B64 $parts[1] # The following could be used to decode the encData (and verify the encryption key) but can't do A256GCM with C# #$IV = Convert-B64ToByteArray -B64 $parts[2] #$encData = Convert-B64ToByteArray -B64 $parts[3] #$tag = Convert-B64ToByteArray -B64 $parts[4] Write-Verbose "JWE: enc=$($header.enc), alg=$($header.alg)" try { # Do the magic $deFormatter = [System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter]::new($privateKey) $deckey = $deFormatter.DecryptKeyExchange($encKey) } catch { throw "Decrypting the session key failed: ""$($_.Exception.InnerException.Message)"". Are you using the correct certificate (transport key)?" } # Return return $decKey } } # Aug 24th 2020 # Derives a 32 byte key using the given context and session key function Get-PRTDerivedKey { [cmdletbinding()] Param( [Parameter(ParameterSetName='Byte',Mandatory=$True)] [byte[]]$Context, [Parameter(ParameterSetName='Byte',Mandatory=$True)] [byte[]]$SessionKey, [Parameter(ParameterSetName='B64',Mandatory=$True)] [string]$B64Context, [Parameter(ParameterSetName='B64',Mandatory=$True)] [string]$B64SessionKey, [Parameter(ParameterSetName='Hex',Mandatory=$True)] [string]$HexContext, [Parameter(ParameterSetName='Hex',Mandatory=$True)] [string]$HexSessionKey ) Process { if($B64Context) { $Context = Convert-B64ToByteArray $B64Context $SessionKey = Convert-B64ToByteArray $B64SessionKey } elseif($HexContext) { $Context = Convert-HexToByteArray $HexContext $SessionKey = Convert-HexToByteArray $HexSessionKey } # Fixed label $label = [text.encoding]::UTF8.getBytes("AzureAD-SecureConversation") # Derive the decryption key using a standard NIST SP 800-108 KDF # As the key size is only 32 bytes (256 bits), no need to loop :) $computeValue = @(0x00,0x00,0x00,0x01) + $label + @(0x00) + $Context + @(0x00,0x00,0x01,0x00) $hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList @(,$SessionKey) $hmacOutput = $hmac.ComputeHash($computeValue) Write-Verbose "DerivedKey: $(Convert-ByteArrayToHex $hmacOutput)" # Return $hmacOutput } } # Get the access token with PRT # Aug 20th 2020 function Get-AccessTokenWithPRT { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$Cookie, [Parameter(Mandatory=$True)] [String]$Resource, [Parameter(Mandatory=$True)] [String]$ClientId, [Parameter(Mandatory=$False)] [String]$RedirectUri="urn:ietf:wg:oauth:2.0:oob", [switch]$GetNonce ) Process { $parsedCookie = Read-Accesstoken $Cookie # Create parameters $mscrid = (New-Guid).ToString() $requestId = $mscrid # Create url and headers $url = "https://login.microsoftonline.com/Common/oauth2/authorize?resource=$Resource&client_id=$ClientId&response_type=code&redirect_uri=$RedirectUri&client-request-id=$requestId&mscrid=$mscrid" # Add sso_nonce if exist if($parsedCookie.request_nonce) { $url += "&sso_nonce=$($parsedCookie.request_nonce)" } $headers = @{ "User-Agent" = "" "x-ms-RefreshTokenCredential" = $Cookie } # First, make the request to get the authorisation code (tries to redirect so throws an error) $response = Invoke-RestMethod -UseBasicParsing -Uri $url -Headers $headers -MaximumRedirection 0 -ErrorAction SilentlyContinue Write-Debug "RESPONSE: $($response.OuterXml)" # Try to parse the code from the response if($response.html.body.script) { $values = $response.html.body.script.Split("?").Split("\") foreach($value in $values) { $row=$value.Split("=") if($row[0] -eq "code") { $code = $row[1] Write-Verbose "CODE: $code" break } } } if(!$code) { if($response.html.body.h2.a.href -ne $null) { $values = $response.html.body.h2.a.href.Split("&") foreach($value in $values) { $row=$value.Split("=") if($row[0] -eq "sso_nonce") { $sso_nonce = $row[1] if($GetNonce) { # Just return the nonce return $sso_nonce } else { # Invalid PRT, nonce is reuired Write-Warning "Nonce needed. Try New-AADIntUserPRTToken with -GetNonce switch or -Nonce $sso_nonce parameter" break } } } } throw "Code not received!" } # Create the body $body = @{ client_id = $ClientId grant_type = "authorization_code" code = $code redirect_uri = $RedirectUri } # Make the second request to get the access token $response = Invoke-RestMethod -UseBasicParsing -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body -ContentType "application/x-www-form-urlencoded" -Method Post Write-Debug "ACCESS TOKEN: $($response.access_token)" Write-Debug "REFRESH TOKEN: $($response.refresh_token)" # Return return $response } } # Get the access token with BPRT # Jan 10th 2021 function Get-AccessTokenWithBPRT { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$BPRT, [Parameter(Mandatory=$True)] [String]$Resource, [Parameter(Mandatory=$True)] [String]$ClientId ) Process { Get-AccessTokenWithRefreshToken -Resource "urn:ms-drs:enterpriseregistration.windows.net" -ClientId "b90d5b8f-5503-4153-b545-b31cecfaece2" -TenantId "Common" -RefreshToken $BPRT } } # Get the token with deviceid claim # Aug 28th function Set-AccessTokenDeviceAuth { [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [bool]$BPRT, [Parameter(Mandatory=$False)] [string]$AccessToken, [Parameter(Mandatory=$True)] [string]$RefreshToken, [Parameter(Mandatory=$False)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [Parameter(Mandatory=$False)] [string]$PfxFileName, [Parameter(Mandatory=$False)] [string]$PfxPassword ) Process { if(!$Certificate) { $Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable } if($BPRT) { # Fixed values for BPRT to get access token for Intune MDM $clientId = "b90d5b8f-5503-4153-b545-b31cecfaece2" $resource = "https://enrollment.manage.microsoft.com/" } else { # This is the only supported client id :( $clientId = "29d9ed98-a469-4536-ade2-f981bc1d605e" # Get the claims from the access token to get the resource $claims = Read-Accesstoken -AccessToken $AccessToken $resource = $claims.aud } # Get the private key $privateKey = Load-PrivateKey -Certificate $Certificate $body=@{ "grant_type" = "srv_challenge" "windows_api_version" = "2.0" "client_id" = $clientId "redirect_uri" = "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS" "resource" = $resource } if($BPRT) { $body.Remove("redirect_uri") } # Get the nonce $nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body).Nonce # B64 encode the public key $x5c = Convert-ByteArrayToB64 -Bytes ($certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) # Create the header and body $hdr = [ordered]@{ "alg" = "RS256" "typ" = "JWT" "x5c" = "$x5c" } $OSVersion="10.0.18362.997" $pld = [ordered]@{ "win_ver" = $OSVersion "resource" = $resource "scope" = "openid aza" "request_nonce" = $nonce "refresh_token" = $RefreshToken "redirect_uri" = "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS" "iss" = "aad:brokerplugin" "grant_type" = "refresh_token" "client_id" = $clientId } if($BPRT) { $pld.Remove("redirect_uri") $pld["scope"] = "openid" } # Create the JWT $jwt = New-JWT -PrivateKey $privateKey -Header $hdr -Payload $pld # Construct the body $body = @{ "windows_api_version" = "2.0" "grant_type" = "urn:ietf:params:oauth:grant-type:jwt-bearer" "request" = "$jwt" } # Make the request to get the new access token $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Body $body if($BPRT) { $response | Add-Member -NotePropertyName "refresh_token" -NotePropertyValue $RefreshToken } Write-Debug "ACCESS TOKEN: $($response.access_token)" Write-Debug "REFRESH TOKEN: $($response.refresh_token)" # Return return $response } } function New-JWT { [cmdletbinding()] Param( [Parameter(ParameterSetName='PrivateKey', Mandatory=$True)] [System.Security.Cryptography.RSA]$PrivateKey, [Parameter(ParameterSetName='Key',Mandatory=$True)] [Byte[]]$Key, [Parameter(Mandatory=$True)] [Hashtable]$Header, [Parameter(Mandatory=$True)] [Hashtable]$Payload ) Process { # Construct the header $txtHeader = ($Header | ConvertTo-Json -Compress).Replace("/","\/") $txtPayload = ($Payload | ConvertTo-Json -Compress).Replace("/","\/") # Convert to B64 and strip the padding $b64Header = (Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.getBytes($txtHeader))).replace("=","") $b64Payload = (Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.getBytes($txtPayload))).replace("=","") # Construct the JWT data to be signed $binData = [text.encoding]::UTF8.GetBytes(("{0}.{1}" -f $b64Header,$b64Payload)) # Get the signature $Binsig = Sign-JWT -PrivateKey $PrivateKey -Key $Key -Data $binData $B64sig = Convert-ByteArrayToB64 -Bytes $Binsig -UrlEncode # Construct the JWT $jwt = "{0}.{1}.{2}" -f $b64Header,$b64Payload,$B64sig # Return return $jwt } } function Get-PRTKeyInfo { [cmdletbinding()] Param( [Parameter(ParameterSetName='PrivateKey',Mandatory=$True)] [byte[]]$PrivateKey ) Process { # Create a random context $ctx = New-Object byte[] 24 ([System.Security.Cryptography.RandomNumberGenerator]::Create()).GetBytes($context) # Get the private key $privateKey = Load-PrivateKey -Certificate $Certificate $body=@{ "grant_type" = "srv_challenge" "windows_api_version" = "2.0" "client_id" = $ClientId "redirect_uri" = $RedirectUri "resource" = $Resource } # Get the nonce $nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body).Nonce # B64 encode the public key $x5c = Convert-ByteArrayToB64 -Bytes ($certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) # Create the header and body $hdr = [ordered]@{ "alg" = "RS256" "typ" = "JWT" "x5c" = "$x5c" } $OSVersion="10.0.18362.997" $pld = [ordered]@{ "win_ver" = $OSVersion "resource" = $Resource "scope" = "openid aza" "request_nonce" = $nonce "refresh_token" = $RefreshToken "redirect_uri" = $RedirectUri "iss" = "aad:brokerplugin" "grant_type" = "refresh_token" "client_id" = $ClientId } # Create the JWT $jwt = New-JWT -PrivateKey $privateKey -Header $hdr -Payload $pld # Construct the body $body = @{ "windows_api_version" = "2.0" "grant_type" = "urn:ietf:params:oauth:grant-type:jwt-bearer" "request" = "$jwt" } # Make the request to get the P2P certificate $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Body $body Write-Debug "ACCESS TOKEN: $($response.access_token)" Write-Debug "REFRESH TOKEN: $($response.refresh_token)" # Return return $response } } |