Private/PSPkinit/Invoke-PkinitAuthentication.ps1
|
<#
Invoke-PkinitAuthentication: public entry point for PSPkinit. #> function Invoke-PkinitAuthentication { <# .SYNOPSIS Performs a full RFC 4556 PKINIT AS-REQ/AS-REP exchange against a KDC using the Diffie-Hellman key delivery method, and returns the decrypted identity/session information from a successful AS-REP. .DESCRIPTION Wraps the full PKINIT client flow - building a signed AuthPack, sending an AS-REQ, and parsing/decrypting the resulting AS-REP - behind a single cmdlet. Intended for validating that a certificate, CA, and KDC configuration actually support PKINIT end-to-end (cert chain, EKU, SID security extension, strong-mapping enforcement, etc.), not as a general-purpose Kerberos client. This only ever authenticates as the identity bound to the supplied certificate's own private key - it does not forge, inject, or reuse anyone else's tickets or credentials. .PARAMETER CertificateThumbprint Thumbprint of a certificate (with its private key) in Cert:\CurrentUser\My to authenticate with. .PARAMETER Certificate An already-loaded X509Certificate2 (with private key) to authenticate with, e.g. one you loaded yourself from a PFX or hardware token. .PARAMETER PfxPath Path to a .pfx/.p12 file containing the certificate and private key to authenticate with. .PARAMETER PfxPassword Password protecting the PFX file specified by -PfxPath. .PARAMETER ClientName The client principal name to authenticate as (RFC 4120 cname). For Active Directory, this is typically the user's UPN, e.g. 'user@contoso.com'. .PARAMETER ClientNameType Kerberos name-type for -ClientName (RFC 4120 7.5.8). Defaults to NT-ENTERPRISE (10), which is the reliable choice for a UPN-style -ClientName against Active Directory. Use NT-PRINCIPAL (1) for a bare, single-component name. .PARAMETER Realm The Kerberos realm (typically the uppercase DNS domain name, e.g. 'CONTOSO.COM'). .PARAMETER KdcHostname Hostname or IP address of the KDC to send the AS-REQ to. .PARAMETER Port TCP port the KDC is listening on. Defaults to 88. .PARAMETER EncryptionTypes Encryption types to offer the KDC, in preference order. Defaults to @(18, 17) - aes256-cts-hmac-sha1-96, then aes128-cts-hmac-sha1-96. .PARAMETER Nonce The nonce to use in the KDC-REQ-BODY and PKAuthenticator. Defaults to a fresh cryptographically random value; override only for reproducible troubleshooting/testing. .PARAMETER TimeoutSeconds Timeout for the TCP connection and each read, in seconds. Defaults to 10. .PARAMETER SkipKdcSignatureVerification Skips cryptographic verification of the KDC's CMS signature on the DH reply (KDCDHKeyInfo). The KDC's certificate chain is never validated by this module regardless of this switch - only point it at a KDC you already trust. .OUTPUTS PSCustomObject describing the authenticated identity, ticket validity window, negotiated session key, and the KDC's PKINIT signing certificate. .EXAMPLE Invoke-PkinitAuthentication -CertificateThumbprint '1A49DADC...' -ClientName 'user@adcs.goat' -Realm 'ADCS.GOAT' -KdcHostname 'ADCSGoat-DC.adcs.goat' Authenticates using a certificate already in Cert:\CurrentUser\My. .EXAMPLE $securePassword = Read-Host -AsSecureString -Prompt 'PFX password' Invoke-PkinitAuthentication -PfxPath 'C:\certs\user.pfx' -PfxPassword $securePassword -ClientName 'user@adcs.goat' -Realm 'ADCS.GOAT' -KdcHostname 'ADCSGoat-DC.adcs.goat' Authenticates using a certificate loaded directly from a PFX file. #> [CmdletBinding(DefaultParameterSetName = 'Thumbprint')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, ParameterSetName = 'Thumbprint')] [ValidateNotNullOrEmpty()] [string] $CertificateThumbprint, [Parameter(Mandatory, ParameterSetName = 'Certificate')] [ValidateNotNull()] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(Mandatory, ParameterSetName = 'PfxPath')] [ValidateNotNullOrEmpty()] [string] $PfxPath, [Parameter(Mandatory, ParameterSetName = 'PfxPath')] [ValidateNotNull()] [securestring] $PfxPassword, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ClientName, [Parameter()] [int] $ClientNameType = 10, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Realm, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $KdcHostname, [Parameter()] [int] $Port = 88, [Parameter()] [int[]] $EncryptionTypes = @(18, 17), [Parameter()] [uint32] $Nonce = [uint32](Get-Random -Minimum 1 -Maximum ([int]::MaxValue)), [Parameter()] [int] $TimeoutSeconds = 10, [Parameter()] [switch] $SkipKdcSignatureVerification ) switch ($PSCmdlet.ParameterSetName) { 'Thumbprint' { $resolvedCert = Get-ChildItem -Path 'Cert:\CurrentUser\My' | Where-Object { $_.Thumbprint -eq $CertificateThumbprint } if (-not $resolvedCert) { $exception = [System.Security.Cryptography.CryptographicException]::new( "No certificate with thumbprint '$CertificateThumbprint' was found in Cert:\CurrentUser\My.") $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $exception, 'PkinitCertificateNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $CertificateThumbprint)) } } 'Certificate' { $resolvedCert = $Certificate } 'PfxPath' { if (-not (Test-Path -Path $PfxPath)) { $exception = [System.IO.FileNotFoundException]::new("PFX file not found: $PfxPath", $PfxPath) $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $exception, 'PkinitPfxNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $PfxPath)) } $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable try { try { # .NET 9+ prefers X509CertificateLoader over the (now-obsolete-marked) X509Certificate2 constructors. $resolvedCert = [System.Security.Cryptography.X509Certificates.X509CertificateLoader]::LoadPkcs12FromFile($PfxPath, $PfxPassword, $keyStorageFlags) } catch { # X509CertificateLoader doesn't exist on this runtime (Windows PowerShell 5.1 / older .NET) - fall back. $resolvedCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $PfxPassword, $keyStorageFlags) } } catch { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $_.Exception, 'PkinitPfxLoadFailed', [System.Management.Automation.ErrorCategory]::InvalidData, $PfxPath)) } } } if (-not $resolvedCert.HasPrivateKey) { $exception = [System.Security.Cryptography.CryptographicException]::new( "Certificate '$($resolvedCert.Thumbprint)' does not have a private key available.") $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $exception, 'PkinitCertificateNoPrivateKey', [System.Management.Automation.ErrorCategory]::InvalidOperation, $resolvedCert)) } try { $body = New-KdcReqBody -ClientName $ClientName -ClientNameType $ClientNameType -Realm $Realm -Nonce $Nonce -EncryptionTypes $EncryptionTypes $dh = New-PkinitDiffieHellmanKeyPair $authPack = New-PkinitAuthPack -KdcReqBody $body -Nonce $Nonce -DhKeyPair $dh $signed = New-PkinitSignedAuthPack -AuthPack $authPack -Certificate $resolvedCert $paData = New-PkinitPaData -SignedAuthPack $signed $asReq = New-AsReq -KdcReqBody $body -PaData @($paData) $response = Send-KerberosTcpMessage -KdcHostname $KdcHostname -Port $Port -Message $asReq -TimeoutSeconds $TimeoutSeconds if ($response[0] -eq 0x7e) { $krbError = ConvertFrom-KrbError -Message $response $kdcException = [System.Security.Authentication.AuthenticationException]::new( "KDC rejected the AS-REQ: KRB-ERROR $($krbError.ErrorCode) $($krbError.ErrorText)".Trim()) $kdcException.Data['KrbError'] = $krbError throw $kdcException } $asRep = ConvertFrom-KdcRep -Message $response -ExpectedApplicationTag 11 $pkAsRepPaData = $asRep.PaData | Where-Object { $_.Type -eq 17 } if (-not $pkAsRepPaData) { throw 'AS-REP did not include a PA-PK-AS-REP (type 17) padata element.' } $dhRepInfo = ConvertFrom-PkinitDhRepInfo -PaDataValue $pkAsRepPaData.Value $kdcDhKeyInfo = ConvertFrom-PkinitKdcDhKeyInfo -DhSignedData $dhRepInfo.DhSignedData -SkipSignatureVerification:$SkipKdcSignatureVerification if ($kdcDhKeyInfo.Nonce -ne $Nonce) { throw "Nonce mismatch: sent $Nonce, KDC returned $($kdcDhKeyInfo.Nonce) - possible replay or tampering." } $sharedSecret = Get-PkinitDiffieHellmanSharedSecret -TheirPublicValue $kdcDhKeyInfo.ServerPublicValue -OurPrivateExponent $dh.PrivateExponent -P $dh.P -ModulusByteLength $dh.ModulusByteLength $keyByteLength = switch ($asRep.EncPart.EType) { 18 { 32 } 17 { 16 } default { throw "Unsupported AS-REP enctype $($asRep.EncPart.EType)." } } $replyKey = ConvertTo-OctetString2Key -InputBytes $sharedSecret -KeyByteLength $keyByteLength $decrypted = Unprotect-KerberosData -BaseKey $replyKey -KeyUsage 3 -Ciphertext $asRep.EncPart.Cipher $encPart = ConvertFrom-EncKdcRepPart -Message $decrypted -ExpectedApplicationTag 25 # ESCalator adaptation (verify-only): capture TGT length as proof, then zero the # session key and TGT byte arrays before returning. The live ticket and key are # never injected, written to disk, or returned usable to the caller. $sessionKey = $encPart.Key.KeyValue $ticket = $asRep.Ticket $sessionKeyLength = if ($sessionKey) { $sessionKey.Length } else { 0 } $ticketLength = if ($ticket) { $ticket.Length } else { 0 } $result = [PSCustomObject]@{ PSTypeName = 'PSPkinit.AuthenticationResult' CName = $asRep.CName.NameStrings -join '/' CRealm = $asRep.CRealm SName = $encPart.SName.NameStrings -join '/' SRealm = $encPart.SRealm AuthTime = $encPart.AuthTime StartTime = $encPart.StartTime EndTime = $encPart.EndTime RenewTill = $encPart.RenewTill TicketFlags = $encPart.Flags SessionKeyType = $encPart.Key.KeyType SessionKeyLength = $sessionKeyLength TicketLength = $ticketLength Nonce = $Nonce KdcSignerCertificate = $kdcDhKeyInfo.SignerCertificate Verified = $true } # Zero sensitive material now that the result no longer carries it. if ($sessionKey) { [Array]::Clear($sessionKey, 0, $sessionKey.Length) } if ($ticket) { [Array]::Clear($ticket, 0, $ticket.Length) } if ($replyKey) { [Array]::Clear($replyKey, 0, $replyKey.Length) } if ($sharedSecret) { [Array]::Clear($sharedSecret, 0, $sharedSecret.Length) } return $result } catch [System.Security.Authentication.AuthenticationException] { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $_.Exception, 'PkinitKdcError', [System.Management.Automation.ErrorCategory]::SecurityError, $_.Exception.Data['KrbError'])) } catch { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $_.Exception, 'PkinitAuthenticationFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null)) } } |