Device.ps1

# This file contains functions for local AAD Joined devices

# Exports the device certificate of the local device
# Dec 17th 2021
function Export-LocalDeviceCertificate
{
<#
    .SYNOPSIS
    Exports the device certificate and private key of the local AAD joined/registered device.
 
    .DESCRIPTION
    Exports the device certificate and private key of the local AAD joined/registered device.
    Certificate filename: <deviceid>.pfx
    Private key filename: <deviceid>.pem
 
    .Example
    PS C\:>Export-AADIntLocalDeviceCertificate
 
    Certificate exported to f72ad27e-5833-48d3-b1d6-00b89c429b91.pfx
#>

    [CmdletBinding()]
    param()
    Process
    {
        # Check whether we are running in elevated session
        Test-LocalAdministrator -Warn | Out-Null

        # Get the join info
        if(($joinInfo = Get-LocalDeviceJoinInfo) -eq $null)
        {
            Throw "Device seems not to be joined or registered."
        }

        # Get the certificate
        Write-Verbose "Getting certificate $($joinInfo.CertThumb)"
        $certificate = Get-Item -Path $joinInfo.CertPath
        $binCert     = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)

        # Get the private key
        Write-Verbose "Device key name: $($joinInfo.KeyName)"

        if($joinInfo.JoinType -eq "Joined")
        {
            $keyPath = "$env:ALLUSERSPROFILE"
        }
        else
        {
            Write-Warning "Accessing key in user context will FAIL if already elevated to LOCAL SYSTEM."
            $keyPath = "$env:APPDATA"
        }

        # CryptoAPI and CNG stores keys in different directories
        # https://docs.microsoft.com/en-us/windows/win32/seccng/key-storage-and-retrieval
        $paths = @(
            "$keyPath\Microsoft\Crypto\RSA\MachineKeys\$($joinInfo.KeyName)"
            "$keyPath\Microsoft\Crypto\Keys\$($joinInfo.KeyName)"
            )
        foreach($path in $paths)
        {
            $keyBlob = Get-Content $path -Encoding byte -ErrorAction SilentlyContinue
            if($keyBlob)
            {
                Write-Verbose "Key loaded from $path"
                break
            }
        }

        if(!$keyBlob)
        {
            if($joinInfo.KeyName.EndsWith(".PCPKEY"))
            {
                # This machine has a TPM
                Throw "PCP keys are not supported, unable to export private key!"
            }
            else
            {
                Throw "Error accessing key. If you are already elevated to LOCAL SYSTEM, restart PowerShell and try again."
            }
            return
        }
        
        # Parse the key blob
        $blobType = [System.BitConverter]::ToInt32($keyBlob,0)
        switch($blobType)
        {
            1 { $deviceKey = Parse-CngBlob  -Data $keyBlob -Decrypt }
            2 { $deviceKey = Parse-CapiBlob -Data $keyBlob -Decrypt }
            default { throw "Unsupported key blob type" }
        }

        $fileName = "$($joinInfo.deviceId).pfx"
        Set-Content $fileName -Value (New-PfxFile -RSAParameters $deviceKey.RSAParameters -X509Certificate $binCert) -Encoding Byte
        Write-Host "Device certificate exported to $fileName"
    }
}

# Exports the transport key of the local device
# Dec 18th 2021
function Export-LocalDeviceTransportKey
{
<#
    .SYNOPSIS
    Exports the transport key of the local AAD joined/registered device.
 
    .DESCRIPTION
    Exports the transport key of the local AAD joined/registered device.
    Filename: <deviceid>_tk.pem
 
    .Example
    PS C\:>Export-AADIntLocalDeviceTransportKey
 
    Transport key exported to f72ad27e-5833-48d3-b1d6-00b89c429b91_tk.pem
#>

    [CmdletBinding()]
    param()
    Process
    {
        # Check whether we are running in elevated session
        Test-LocalAdministrator -Warn | Out-Null

        # Get the join info
        if(($joinInfo = Get-LocalDeviceJoinInfo) -eq $null)
        {
            Throw "Device seems not to be joined or registered."
        }

        # Get the private key
        Write-Verbose "Getting transport key"
        $transportKeys = Get-LocalDeviceTransportKeys -JoinType $joinInfo.JoinType -IdpDomain $joinInfo.idpDomain -TenantId $joinInfo.tenantId -UserEmail $joinInfo.UserEmail

        $fileName = "$($joinInfo.deviceId)_tk.pem"
        Set-Content $fileName -Value (Convert-RSAToPEM -RSAParameters $transportKeys.RSAParameters)
        Write-Host "Transport key exported to $fileName"

    }
}

# Joins the local device to Azure AD
# Dec 20th 2021
function Join-LocalDeviceToAzureAD
{
<#
    .SYNOPSIS
    Joins the local Windows device to Azure AD using the given certificate.
 
    .DESCRIPTION
    Joins the local Windows device to Azure AD using the given certificate created earlier with AADInternals.
 
    Creates required registry keys and values, saves transport key to SystemKeys, and starts related scheduled tasks.
     
    .Parameter OSVersion
    The operating system version of the device. Defaults to "10.0.18363.0"
 
    .Parameter PfxFileName
    File name of the .pfx device certificate.
 
    .Parameter PfxPassword
    The password of the .pfx device certificate.
 
    .Parameter TransportKeyFileName
    File name of the transportkey
 
    .Parameter UserPrincipalName
    The user principal name of the user.
 
    .EXAMPLE
    PS\:>Export-AADIntLocalDeviceCertificate
 
    Certificate exported to f72ad27e-5833-48d3-b1d6-00b89c429b91.pfx
 
    PS C\:>Export-AADIntLocalDeviceTransportKey
 
    Transport key exported to f72ad27e-5833-48d3-b1d6-00b89c429b91_tk.pem
 
    PS\:>Join-AADIntLocalDeviceToAzureAD -UserPrincipalName JohnD@company.com -PfxFileName .\f72ad27e-5833-48d3-b1d6-00b89c429b91.pfx -TransportKeyFileName .\f72ad27e-5833-48d3-b1d6-00b89c429b91_tk.pem
 
    Device configured. To confirm success, restart and run: dsregcmd /status
 
    .EXAMPLE
    $token = Get-AADIntAccessTokenForAADJoin -SaveToCache
    PS\:>Join-AADIntDeviceToAzureAD -DeviceName "My computer" -DeviceType "Commodore" -OSVersion "C64"
 
    Device successfully registered to Azure AD:
      DisplayName: "My computer"
      DeviceId: d03994c9-24f8-41ba-a156-1805998d6dc7
      Cert thumbprint: 78CC77315A100089CF794EE49670552485DE3689
      Cert file name : "d03994c9-24f8-41ba-a156-1805998d6dc7.pfx"
    Local SID:
      S-1-5-32-544
    Additional SIDs:
      S-1-12-1-797902961-1250002609-2090226073-616445738
      S-1-12-1-3408697635-1121971140-3092833713-2344201430
      S-1-12-1-2007802275-1256657308-2098244751-2635987013
 
    PS\:>Join-AADIntLocalDeviceToAzureAD -UserPrincipalName JohnD@company.com -PfxFileName .\d03994c9-24f8-41ba-a156-1805998d6dc7.pfx
 
    Device configured. To confirm success, restart and run: dsregcmd /status
 
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$UserPrincipalName,
        [Parameter(Mandatory=$True)]
        [String]$PfxFileName,
        [Parameter(Mandatory=$False)]
        [String]$PfxPassword,
        [Parameter(Mandatory=$False)]
        [String]$TransportKeyFileName,
        [Parameter(Mandatory=$False)]
        [String]$OSVersion = "10.0.19044.1288"
    )
    Begin
    {
        $sha256 = [System.Security.Cryptography.SHA256]::Create()
    }
    Process
    {
        # Check whether we are running in elevated session
        Test-LocalAdministrator -Throw | Out-Null

        # Import the certificate to LocalMachine's Personal store
        if($PfxPassword)
        {
            $certificate = Import-PfxCertificate -FilePath $PfxFileName -Password ($PfxPassword | ConvertTo-SecureString -AsPlainText -Force) -CertStoreLocation Cert:\LocalMachine\My -Exportable
        }
        else
        {
            $certificate = Import-PfxCertificate -FilePath $PfxFileName -CertStoreLocation Cert:\LocalMachine\My -Exportable
        }
        Write-Verbose "Certificate ($($certificate.Subject)) imported to CERT:\LocalMachine\My\$($certificate.Thumbprint)"
        
        # Collect the required information
        $thumbprint = $certificate.Thumbprint
        $oids = Parse-CertificateOIDs -Certificate $certificate
        $tenantId = $oids.TenantId
        $deviceId = $oids.DeviceId

        Write-Verbose "Thumbprint: $thumbprint"
        Write-Verbose "Device ID: $deviceId"
        Write-Verbose "Tenant ID: $tenantId"
        Write-Verbose "Object ID: $($oids.ObjectId)"
        Write-Verbose "Region: $($oids.Region)"
        Write-Verbose "Join Type: $($oids.JoinType)"

        if($oids.JoinType -eq 0)
        {
            # Certificates for AAD Registered devices won't work :(
            Remove-Item $certificate -Force
            Throw "Unable to join: Provided certificate is for AAD Registered device."
        }

        # Generate P2P cert and CA & import to correct stores
        Write-Verbose "Generating P2P certificate & CA"
        New-P2PDeviceCertificate -PfxFileName $PfxFileName -TenantId $tenantId -DeviceName (Get-ComputerName) 

        $P2P = Import-PfxCertificate -FilePath ".\$($deviceId)-P2P.pfx" -CertStoreLocation "Cert:\LocalMachine\My" -Exportable
        Write-Verbose "Certificate ($($P2P.Subject)) imported to CERT:\LocalMachine\My\$($P2P.Thumbprint)"

        if(-not (Test-Path "Cert:\LocalMachine\AAD Token Issuer"))
        {
            New-Item -Path "Cert:\LocalMachine" -Name "AAD Token Issuer" -ItemType "directory" -Force 
        }

        $P2PCA = Import-Certificate -FilePath ".\$($deviceId)-P2P-CA.der" -CertStoreLocation "Cert:\LocalMachine\AAD Token Issuer"
        Write-Verbose "Certificate ($($P2PCA.Subject)) imported to CERT:\LocalMachine\AAD Token Issuer\$($P2PCA.Thumbprint)"

        # Generate the transport key using device id as name
        if($TransportKeyFileName)
        {
            # Use the provided tkpriv
            $tkPEM = (Get-Content $TransportKeyFileName) -join "`n"
            $tkParameters = Convert-PEMToRSA -PEM $tkPEM
        }
        else
        {
            # Use dkpriv from the certificate
            $tkParameters = $certificate.PrivateKey.ExportParameters($true)
        }
        $transportKeyName   = $deviceId
        $RSAFULLPRIVATEBLOB = New-KeyBLOB -Parameters $tkParameters -Type RSA3

        $cngParameters=[System.Security.Cryptography.CngKeyCreationParameters]::new()
        $cngParameters.KeyCreationOptions = 0x20 -bor 0x80 # Create machine key | overwrite
        $cngParameters.Parameters.Add([System.Security.Cryptography.CngProperty]::new("Length",[System.BitConverter]::GetBytes(2048),"None"))
        $cngParameters.Parameters.Add([System.Security.Cryptography.CngProperty]::new("RSAFULLPRIVATEBLOB",$RSAFULLPRIVATEBLOB,"None"))
        $cngParameters.ExportPolicy = 0x01 -bor 0x02 # Allow export, allow plaintext export
        $transportKey = [System.Security.Cryptography.CngKey]::Create("RSA",$transportKeyName,$cngParameters)

        Write-Verbose "TransportKey name: $($transportKey.KeyName)"
        Write-Verbose "TransportKey file name: $($transportKey.UniqueName)"

        # Copy the private key to SystemKeys folder & delete from the current location
        Copy-Item -Path "$env:ALLUSERSPROFILE\Microsoft\Crypto\Keys\$($transportKey.UniqueName)" -Destination "$env:ALLUSERSPROFILE\Microsoft\Crypto\SystemKeys" -Force
        Write-Verbose "Transport key stored to $env:ALLUSERSPROFILE\Microsoft\Crypto\SystemKeys\$($transportKey.UniqueName)"
        $transportKey.Delete()

        # Create the registry keys
        $CloudDomainJoinRoot = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin"

        New-Item -Path "$CloudDomainJoinRoot"            -Name "JoinInfo"   -Force | Out-Null
        New-Item -Path "$CloudDomainJoinRoot\JoinInfo"   -Name $thumbprint  -Force | Out-Null
        New-Item -Path "$CloudDomainJoinRoot"            -Name "TenantInfo" -Force | Out-Null
        New-Item -Path "$CloudDomainJoinRoot\TenantInfo" -Name $TenantId    -Force | Out-Null

        # Set join info
        $joinInfo = @{
            "IdpDomain"          = "login.windows.net"
            "TenantId"           = $TenantId
            "UserEmail"          = $UserPrincipalName
            "AttestationLevel"   = 0
            "AikCertStatus"      = 0
            "TransportKeyStatus" = 0
            "DeviceDisplayName"  = Get-ComputerName
            "OsVersion"          = $OSVersion
            "DdidUpToDate"       = 0
            "LastSyncTime"       = [int]((Get-Date).ToUniversalTime()-$epoch).TotalSeconds
        }

        Write-Verbose "Created key $CloudDomainJoinRoot\JoinInfo\$thumbprint"

        foreach($key in $joinInfo.Keys)
        {
            Set-ItemProperty -Path "$CloudDomainJoinRoot\JoinInfo\$thumbprint" -Name $key -Value $joinInfo[$key] | Out-Null
            Write-Verbose " $key = $($joinInfo[$key])"
            
        }

        # Set tenant info
        $tenantInfo = @{
            "DisplayName"                    = $UserPrincipalName.split("@")[1].Split(".")[0]
            "MdmEnrollmentUrl"               = ""
            "MdmTermsOfUseUrl"               = ""
            "MdmComplianceUrl"               = ""
            "UserSettingSyncUrl"             = ""
            "DrsServiceVersion"              = "1.0"
            "DrsEndpoint"                    = "https://enterpriseregistration.windows.net/EnrollmentServer/DeviceEnrollmentWebService.svc"
            "DrsResourceId"                  = "urn:ms-drs:enterpriseregistration.windows.net"
            "AuthCodeUrl"                    = "https://login.microsoftonline.com/$tenantId/oauth2/authorize"
            "AccessTokenUrl"                 = "https://login.microsoftonline.com/$tenantId/oauth2/token"
            "CdjServiceVersion"              = "2.0"
            "CdjEndpoint"                    = "https://enterpriseregistration.windows.net/EnrollmentServer/device/"
            "CdjResourceId"                  = "urn:ms-drs:enterpriseregistration.windows.net"
            "NgcServiceVersion"              = "1.0"
            "NgcEndpoint"                    = "https://enterpriseregistration.windows.net/EnrollmentServer/key/"
            "NgcResourceId"                  = "urn:ms-drs:enterpriseregistration.windows.net"
            "WebAuthnServiceVersion"         = "1.0"
            "WebAuthnEndpoint"               = "https://enterpriseregistration.windows.net/webauthn/$tenantId/"
            "WebAuthnResourceId"             = "urn:ms-drs:enterpriseregistration.windows.net"
            "DeviceManagementServiceVersion" = "1.0"
            "DeviceManagementEndpoint"       = "https://enterpriseregistration.windows.net/manage/$tenantId/"
            "DeviceManagementResourceId"     = "urn:ms-drs:enterpriseregistration.windows.net"
            "RbacPolicyEndpoint"             = "https://pas.windows.net"
        }

        Write-Verbose "Created key $CloudDomainJoinRoot\TenantInfo\$tenantId"
        foreach($key in $tenantInfo.Keys)
        {
            Set-ItemProperty -Path "$CloudDomainJoinRoot\TenantInfo\$tenantId" -Name $key -Value $tenantInfo[$key] | Out-Null
            Write-Verbose " $key = $($tenantInfo[$key])"
            
        }

        # Calculate registry key parts for transportkey
        $idp    = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes("login.windows.net")))
        $tenant = Convert-ByteArrayToHex -Bytes ($sha256.ComputeHash([text.encoding]::Unicode.GetBytes($TenantId)))

        # Set the transport key name
        New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey" -Name ""  -Force | Out-Null
        New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$idp" -Name $tenant  -Force | Out-Null
        Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$idp\$tenant" -Name "SoftwareKeyTransportKeyName" -Value $transportKeyName -Force | Out-Null

        Write-Verbose "Transport key set: HKLM:\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\$idp\$tenant\SoftwareKeyTransportKeyName = $transportKeyName"

        # Set some registry values
        Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\IdentityStore\LoadParameters\{B16898C6-A148-4967-9171-64D755DA8520}" -Name "LoginUri" -Value "https://login.microsoftonline.com" -Force | Out-Null
        Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\IdentityStore\LoadParameters\{B16898C6-A148-4967-9171-64D755DA8520}" -Name "Enabled" -Value 1 | Out-Null
        Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Provisioning\AutopilotPolicy" -Name "AutopilotMode" -Value 0 -Force | Out-Null

        # Restart Software Protection Platform
        Start-ScheduledTask  -TaskPath "\Microsoft\Windows\SoftwareProtectionPlatform\" -TaskName "SvcRestartTask"
        Write-Verbose "Restarted Software Protection Platform task."

        # Enable and start AAD Device-Sync
        Enable-ScheduledTask -TaskPath "\Microsoft\Windows\Workplace Join\" -TaskName "Device-Sync" | Out-Null
        Start-ScheduledTask  -TaskPath "\Microsoft\Windows\Workplace Join\" -TaskName "Device-Sync"
        Write-Verbose "Enabled and started Device-Sync task."

        # Run RegisterDeviceProtectionStateChanged
        Start-ScheduledTask  -TaskPath "\Microsoft\Windows\DeviceDirectoryClient\" -TaskName "RegisterDeviceProtectionStateChanged"
        Write-Verbose "Ran RegisterDeviceProtectionStateChanged task."

        Write-Host "Device configured. To confirm success, restart and run: dsregcmd /status"
    }
    End
    {
        $sha256.Dispose()
    }
}

# Gets the join info of the local device
# Dec 23rd 2021
function Get-LocalDeviceJoinInfo
{
<#
    .SYNOPSIS
    Shows the Azure AD Join information of the local device.
 
    .DESCRIPTION
    Shows the Azure AD Join information of the local device.
 
    .Example
    PS C\:>Get-AADIntLocalDeviceJoinInfo
 
    JoinType : Joined
    RegistryRoot : HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin
    CertThumb : CEC55C2566633AC8DA3D9E3EAD98A599084D0C4C
    CertPath : Cert:\LocalMachine\My\CEC55C2566633AC8DA3D9E3EAD98A599084D0C4C
    TenantId : afdb4be1-057f-4dc1-98a9-327ffa079cca
    DeviceId : f4a4ea70-b196-4305-9531-018c3bcfc112
    ObjectId : d625e2e9-8465-4513-b6c9-8d34a3735d41
    KeyName : 8bff0b7f02f6256b521de95a77d4e70d_934bc9f7-04ef-43d8-a343-610b736a4030
    KeyFriendlyName : Device Identity Key
    IdpDomain : login.windows.net
    UserEmail : JohnD@company.com
    AttestationLevel : 0
    AikCertStatus : 0
    TransportKeyStatus : 0
    DeviceDisplayName : WIN-JohnD
    OsVersion : 10.0.19044.1288
    DdidUpToDate : 0
    LastSyncTime : 1643370347
 
    .Example
    PS C\:>Get-AADIntLocalDeviceJoinInfo
    WARNING: This device has a TPM, exporting keys probably does not work!
 
    JoinType : Joined
    RegistryRoot : HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin
    CertThumb : FFDABA36622C66F1F9104703D77603AE1964E92B
    CertPath : Cert:\LocalMachine\My\FFDABA36622C66F1F9104703D77603AE1964E92B
    TenantId : afdb4be1-057f-4dc1-98a9-327ffa079cca
    DeviceId : e4c56ee8-419a-4421-bff4-1d3cb1c85ead
    ObjectId : b62a31e9-8268-485f-aba8-69696cdf3048
    KeyName : C:\ProgramData\Microsoft\Crypto\PCPKSP\[redacted]\[redacted].PCPKEY
    KeyFriendlyName : Device Identity Key
    IdpDomain : login.windows.net
    UserEmail : package_c1b50acc-82f6-4a19-ba87-e62e5f7fbeee@company.com
    AttestationLevel : 0
    AikCertStatus : 0
    TransportKeyStatus : 3
    DeviceDisplayName : cloudpc-80153
    OsVersion : 10.0.19044.1469
    DdidUpToDate : 0
    LastSyncTime : 1643622945
#>

    [CmdletBinding()]
    param()
    Process
    {
        $AADJoinRoot       = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin"
        $AADRegisteredRoot = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin"

        # Check the join type and construct return value
        if(Test-Path -Path "$AADJoinRoot\JoinInfo")
        {
            $joinRoot = $AADJoinRoot
            $certRoot = "LocalMachine"
            $attributes = [ordered]@{
                "JoinType"     = "Joined"
                "RegistryRoot" = $AADJoinRoot
            }
        }
        elseif(Test-Path -Path "$AADRegisteredRoot\JoinInfo")
        {
            $joinRoot = $AADRegisteredRoot
            $certRoot = "CurrentUser"
            $attributes = [ordered]@{
                "JoinType"     = "Registered"
                "RegistryRoot" = $AADRegisteredRoot
            }
        }
        else
        {
            return $null
        }
        
        # Get the Device certificate thumbnail from registery (assuming the device can only be joined once)
        $regItem = (Get-ChildItem -Path "$joinRoot\JoinInfo\").Name
        $certThumbnail = $regItem.Substring($regItem.LastIndexOf("\")+1)
        $certificate   = Get-Item -Path "Cert:\$certRoot\My\$certThumbnail"

        $oids = Parse-CertificateOIDs -Certificate $certificate

        $attributes["CertThumb"      ] = "$certThumbnail"
        $attributes["CertPath"       ] = "Cert:\$certRoot\My\$certThumbnail"
        $attributes["TenantId"       ] = $oids.TenantId
        $attributes["DeviceId"       ] = $oids.DeviceId
        $attributes["ObjectId"       ] = $oids.ObjectId

        # This will fail for DeviceTransportKey because running as Local System
        try
        {
            $attributes["KeyName"        ] = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate).key.uniquename
            $attributes["KeyFriendlyName"] = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate).key.uipolicy.FriendlyName
        }
        catch
        {
            # Okay
        }

        # Read the join info
        $regItem = Get-Item -Path "$joinRoot\JoinInfo\$certThumbnail"
        $valueNames = $regItem.GetValueNames()
        foreach($name in $valueNames)
        {
            $attributes[$name] = $regItem.GetValue($name)
        }

        # Check the TPM
        if($attributes["TransportKeyStatus"] -eq 3)
        {
            Write-Warning "Transport key stored to TPM, exporting not possible!"
        }

        return New-Object psobject -Property $attributes
    }
}