Private/PSPkinit/Pkinit.ps1
|
<#
PKINIT (RFC 4556) specific pieces: Diffie-Hellman key exchange (Diffie-Hellman key delivery method, section 3.2.3.1), AuthPack/PKAuthenticator construction, and CMS SignedData signing/parsing via the .NET System.Security.Cryptography.Pkcs APIs (so we don't have to hand-roll PKCS#7/CMS ASN.1 on top of everything else). #> # System.Security.Cryptography.Pkcs types (ContentInfo, SignedCms, CmsSigner) live in # different assemblies depending on runtime: the "System.Security" assembly on .NET # Framework / Windows PowerShell 5.1, versus "System.Security.Cryptography.Pkcs" on # .NET (Core) - neither is guaranteed to be loaded automatically, so try both. foreach ($assemblyName in 'System.Security', 'System.Security.Cryptography.Pkcs') { try { Add-Type -AssemblyName $assemblyName -ErrorAction Stop } catch { # fall through - the other assembly name (or an already-loaded copy) may work } } # RFC 3526 section 3: 2048-bit MODP Group 14. Required support per RFC 4556 3.2.1. $script:OakleyGroup14PrimeHex = ( 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1' + '29024E088A67CC74020BBEA63B139B22514A08798E3404DD' + 'EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245' + 'E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D' + 'C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F' + '83655D23DCA3AD961C62F356208552BB9ED529077096966D' + '670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' + 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9' + 'DE2BCBF6955817183995497CEA956AE515D2261898FA0510' + '15728E5A8AACAA68FFFFFFFFFFFFFFFF' ) -replace '\s', '' # ESCalator addition: correct ModPow for NetFX 4.8.1 (System.Numerics.BigInteger.ModPow # is broken there). Dot-source the helper so ESCalator.Pkinit.DhBigInteger is available. . (Join-Path $PSScriptRoot 'PkinitModPow.ps1') function ConvertFrom-HexString { [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [string]$Hex ) $clean = $Hex -replace '\s', '' $bytes = New-Object byte[] ($clean.Length / 2) for ($i = 0; $i -lt $bytes.Length; $i++) { $bytes[$i] = [Convert]::ToByte($clean.Substring($i * 2, 2), 16) } return $bytes } function ConvertTo-UnsignedBigInteger { <# .SYNOPSIS Builds a positive System.Numerics.BigInteger from big-endian bytes (which is how DH moduli/values and DER INTEGER content are naturally expressed). #> [CmdletBinding()] param( [Parameter(Mandatory)] [byte[]]$BigEndianBytes ) $little = [byte[]]$BigEndianBytes.Clone() [Array]::Reverse($little) if ($little[$little.Length - 1] -ge 0x80) { $little += [byte]0 } return [System.Numerics.BigInteger]::new($little) } function ConvertTo-BigEndianBytes { <# .SYNOPSIS Converts a non-negative BigInteger to minimal big-endian bytes (no sign byte unless the value's own bits require it to stay non-negative under DER rules - callers needing a fixed-width field should pad separately). #> [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [System.Numerics.BigInteger]$Value ) $bytes = $Value.ToByteArray() [Array]::Reverse($bytes) while ($bytes.Length -gt 1 -and $bytes[0] -eq 0) { $bytes = $bytes[1..($bytes.Length - 1)] } return $bytes } function Invoke-PkinitModPow { <# .SYNOPSIS ESCalator addition: (Base ^ Exponent) mod Modulus using the corrected NetFX-safe big-integer (ESCalator.Pkinit.DhBigInteger), returning a BigInteger to match the surrounding PSPkinit call sites. Replaces System.Numerics.BigInteger.ModPow, which returns wrong answers on .NET Framework 4.8.1. #> [CmdletBinding()] [OutputType([System.Numerics.BigInteger])] param( [Parameter(Mandatory)] [System.Numerics.BigInteger]$Base, [Parameter(Mandatory)] [System.Numerics.BigInteger]$Exponent, [Parameter(Mandatory)] [System.Numerics.BigInteger]$Modulus ) $resultBytes = [ESCalator.Pkinit.DhBigInteger]::ModPowBytes( (ConvertTo-BigEndianBytes -Value $Base), (ConvertTo-BigEndianBytes -Value $Exponent), (ConvertTo-BigEndianBytes -Value $Modulus) ) return ConvertTo-UnsignedBigInteger -BigEndianBytes $resultBytes } function New-PkinitDiffieHellmanKeyPair { <# .SYNOPSIS Generates a client Diffie-Hellman key pair using Oakley Group 14 (2048-bit MODP, RFC 3526), per RFC 4556 3.2.1 item 8. .OUTPUTS PSCustomObject with P, G, Q, PrivateExponent, PublicValue (all BigInteger), and ModulusByteLength (int). #> [CmdletBinding()] param() $p = ConvertTo-UnsignedBigInteger -BigEndianBytes (ConvertFrom-HexString -Hex $script:OakleyGroup14PrimeHex) $g = [System.Numerics.BigInteger]2 $q = ($p - 1) / 2 $modulusByteLength = (ConvertFrom-HexString -Hex $script:OakleyGroup14PrimeHex).Length # Per RFC 4556/RFC 3766: exponent should have at least twice the bit-length of the # symmetric key it will protect (256 bits for AES256) - use a full-width exponent # (same size as the modulus) for a comfortable margin, uniformly random in [2, p-2]. $privateBytes = New-Object byte[] $modulusByteLength $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($privateBytes) } finally { $rng.Dispose() } $privateBytes[0] = $privateBytes[0] -band 0x7f # keep positive / well below p $x = ConvertTo-UnsignedBigInteger -BigEndianBytes $privateBytes $x = ($x % ($p - 3)) + 2 $y = Invoke-PkinitModPow -Base $g -Exponent $x -Modulus $p return [PSCustomObject]@{ P = $p G = $g Q = $q PrivateExponent = $x PublicValue = $y ModulusByteLength = $modulusByteLength } } function Get-PkinitDiffieHellmanSharedSecret { <# .SYNOPSIS Computes DHSharedSecret = ZZ (RFC 2631 2.1.1: theirPublicValue^ourPrivateExponent mod p), padded with leading zeros to the modulus byte length (RFC 4556 3.2.3.1). #> [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [System.Numerics.BigInteger]$TheirPublicValue, [Parameter(Mandatory)] [System.Numerics.BigInteger]$OurPrivateExponent, [Parameter(Mandatory)] [System.Numerics.BigInteger]$P, [Parameter(Mandatory)] [int]$ModulusByteLength ) $zz = Invoke-PkinitModPow -Base $TheirPublicValue -Exponent $OurPrivateExponent -Modulus $P $bytes = ConvertTo-BigEndianBytes -Value $zz if ($bytes.Length -lt $ModulusByteLength) { $padded = New-Object byte[] $ModulusByteLength [Array]::Copy($bytes, 0, $padded, $ModulusByteLength - $bytes.Length, $bytes.Length) $bytes = $padded } return $bytes } function ConvertTo-DhSubjectPublicKeyInfo { <# .SYNOPSIS Builds the clientPublicValue [1] SubjectPublicKeyInfo field of AuthPack for the Diffie-Hellman case, per RFC 3279 section 2.3.3. SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier { id-dhpublicnumber, DomainParameters }, subjectPublicKey BIT STRING -- DER INTEGER(y), wrapped } DomainParameters ::= SEQUENCE { p INTEGER, g INTEGER, q INTEGER } #> [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [PSObject]$DhKeyPair ) $domainParams = ConvertTo-Asn1Sequence -Children @( (ConvertTo-Asn1Integer -Bytes (ConvertTo-BigEndianBytes -Value $DhKeyPair.P)) (ConvertTo-Asn1Integer -Bytes (ConvertTo-BigEndianBytes -Value $DhKeyPair.G)) (ConvertTo-Asn1Integer -Bytes (ConvertTo-BigEndianBytes -Value $DhKeyPair.Q)) ) $algorithmIdentifier = ConvertTo-Asn1Sequence -Children @( (ConvertTo-Asn1Oid -Dotted '1.2.840.10046.2.1') $domainParams ) $publicValueInteger = ConvertTo-Asn1Integer -Bytes (ConvertTo-BigEndianBytes -Value $DhKeyPair.PublicValue) $subjectPublicKey = ConvertTo-Asn1BitString -Bytes $publicValueInteger return ConvertTo-Asn1Sequence -Children @($algorithmIdentifier, $subjectPublicKey) } function New-PkinitAuthPack { <# .SYNOPSIS Builds the DER-encoded AuthPack (RFC 4556 3.2.1) for the Diffie-Hellman key delivery method: PKAuthenticator + clientPublicValue. #> [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [byte[]]$KdcReqBody, [Parameter(Mandatory)] [uint32]$Nonce, [Parameter(Mandatory)] [PSObject]$DhKeyPair, [Parameter()] [datetime]$Time = (Get-Date).ToUniversalTime() ) $paChecksum = [System.Security.Cryptography.SHA1]::Create().ComputeHash($KdcReqBody) $cusec = $Time.Millisecond * 1000 $pkAuthenticator = ConvertTo-Asn1Sequence -Children @( (ConvertTo-Asn1ContextExplicit -TagNumber 0 -InnerTlv (ConvertTo-Asn1Integer -Value ([long]$cusec))) (ConvertTo-Asn1ContextExplicit -TagNumber 1 -InnerTlv (ConvertTo-Asn1GeneralizedTime -Value $Time)) (ConvertTo-Asn1ContextExplicit -TagNumber 2 -InnerTlv (ConvertTo-Asn1Integer -Value ([long]$Nonce))) (ConvertTo-Asn1ContextExplicit -TagNumber 3 -InnerTlv (ConvertTo-Asn1OctetString -Bytes $paChecksum)) ) $clientPublicValue = ConvertTo-DhSubjectPublicKeyInfo -DhKeyPair $DhKeyPair return ConvertTo-Asn1Sequence -Children @( (ConvertTo-Asn1ContextExplicit -TagNumber 0 -InnerTlv $pkAuthenticator) (ConvertTo-Asn1ContextExplicit -TagNumber 1 -InnerTlv $clientPublicValue) ) } function New-PkinitSignedAuthPack { <# .SYNOPSIS CMS SignedData-wraps an AuthPack using the client's certificate (RFC 4556 3.2.1 items 1-7), via .NET's SignedCms so we don't have to hand-roll CMS. Returns the DER-encoded ContentInfo bytes to place in PA-PK-AS-REQ's signedAuthPack [0] IMPLICIT OCTET STRING field. #> [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [byte[]]$AuthPack, [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [Parameter()] [System.Security.Cryptography.Oid]$DigestAlgorithm = [System.Security.Cryptography.Oid]::new('2.16.840.1.101.3.4.2.1') # SHA-256 ) $idPkinitAuthData = '1.3.6.1.5.2.3.1' $contentInfo = [System.Security.Cryptography.Pkcs.ContentInfo]::new( [System.Security.Cryptography.Oid]::new($idPkinitAuthData), $AuthPack ) $signedCms = [System.Security.Cryptography.Pkcs.SignedCms]::new($contentInfo, $false) $signer = [System.Security.Cryptography.Pkcs.CmsSigner]::new( [System.Security.Cryptography.Pkcs.SubjectIdentifierType]::IssuerAndSerialNumber, $Certificate ) $signer.DigestAlgorithm = $DigestAlgorithm $signer.IncludeOption = [System.Security.Cryptography.X509Certificates.X509IncludeOption]::EndCertOnly $signedCms.ComputeSignature($signer, $false) return $signedCms.Encode() } function New-PkinitPaData { <# .SYNOPSIS Builds the PA_PK_AS_REQ (padata-type 16) PA-DATA element: PA-PK-AS-REQ ::= SEQUENCE { signedAuthPack [0] IMPLICIT OCTET STRING } #> [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory)] [byte[]]$SignedAuthPack ) $paPkAsReq = ConvertTo-Asn1Sequence -Children @( (ConvertTo-Asn1ContextImplicitPrimitive -TagNumber 0 -Content $SignedAuthPack) ) return New-PaData -PaDataType 16 -PaDataValue $paPkAsReq } function ConvertFrom-PkinitDhRepInfo { <# .SYNOPSIS Parses the PA-PK-AS-REP (padata-type 17) padata-value for the Diffie-Hellman case (RFC 4556 3.2.3): PA-PK-AS-REP ::= CHOICE { dhInfo [0] DHRepInfo, encKeyPack [1] IMPLICIT OCTET STRING } DHRepInfo ::= SEQUENCE { dhSignedData [0] IMPLICIT OCTET STRING, serverDHNonce [1] DHNonce OPTIONAL } Only the dhInfo choice (tag [0]) is supported, since this module only implements the Diffie-Hellman key delivery method - throws if the KDC instead chose encKeyPack (tag [1], the key-transport/reuse case). .OUTPUTS PSCustomObject with DhSignedData (byte[], the raw CMS ContentInfo DER to pass to ConvertFrom-PkinitKdcDhKeyInfo) and ServerDHNonce (byte[] or $null, only present when DH key reuse was negotiated). #> [CmdletBinding()] param( [Parameter(Mandatory)] [byte[]]$PaDataValue ) $choice = Read-Asn1Tlv -Bytes $PaDataValue -Offset 0 $choiceTagNumber = $choice.Tag -band 0x1f if ($choiceTagNumber -ne 0) { throw "ConvertFrom-PkinitDhRepInfo: expected the dhInfo choice (tag [0]), got tag number $choiceTagNumber (encKeyPack is not supported by this module)." } $dhRepInfoSeq = Read-Asn1Tlv -Bytes $choice.Content -Offset 0 $fields = Read-Asn1Sequence -Content $dhRepInfoSeq.Content $result = [PSCustomObject]@{ DhSignedData = $null; ServerDHNonce = $null } foreach ($field in $fields) { $tagNumber = $field.Tag -band 0x1f switch ($tagNumber) { 0 { $result.DhSignedData = $field.Content } 1 { $result.ServerDHNonce = $field.Content } default { } } } return $result } function ConvertFrom-PkinitKdcDhKeyInfo { <# .SYNOPSIS Decodes the CMS SignedData carried in DHRepInfo.dhSignedData (content type id-pkinit-DHKeyData, RFC 4556 3.2.3.1) via .NET's SignedCms, then parses its inner content: KDCDHKeyInfo ::= SEQUENCE { subjectPublicKey [0] BIT STRING, -- DER INTEGER(y), wrapped nonce [1] INTEGER (0..4294967295), dhKeyExpiration [2] KerberosTime OPTIONAL } .PARAMETER SkipSignatureVerification By default the CMS signature is cryptographically verified (but the signer certificate's chain/revocation status is NOT checked, since this module does not manage a trust store for the lab KDC's cert). Set this switch to skip signature verification entirely. .OUTPUTS PSCustomObject with ServerPublicValue (BigInteger), Nonce (uint32), and SignerCertificate (X509Certificate2, the KDC's PKINIT signing cert). #> [CmdletBinding()] param( [Parameter(Mandatory)] [byte[]]$DhSignedData, [Parameter()] [switch]$SkipSignatureVerification ) $signedCms = [System.Security.Cryptography.Pkcs.SignedCms]::new() $signedCms.Decode($DhSignedData) if (-not $SkipSignatureVerification) { # $true = verify the cryptographic signature only; skip cert chain/revocation checks. $signedCms.CheckSignature($true) } $kdcDhKeyInfo = $signedCms.ContentInfo.Content $seqTlv = Read-Asn1Tlv -Bytes $kdcDhKeyInfo -Offset 0 $fields = Read-Asn1Sequence -Content $seqTlv.Content $result = [PSCustomObject]@{ ServerPublicValue = $null Nonce = $null SignerCertificate = if ($signedCms.SignerInfos.Count -gt 0) { $signedCms.SignerInfos[0].Certificate } else { $null } } foreach ($field in $fields) { $tagNumber = $field.Tag -band 0x1f $inner = Read-Asn1Tlv -Bytes $field.Content -Offset 0 switch ($tagNumber) { 0 { # subjectPublicKey BIT STRING wraps a DER INTEGER: [unused-bits octet][INTEGER TLV]. $integerTlv = Read-Asn1Tlv -Bytes $inner.Content -Offset 1 $result.ServerPublicValue = ConvertTo-UnsignedBigInteger -BigEndianBytes $integerTlv.Content } 1 { $result.Nonce = [uint32](ConvertFrom-Asn1Integer -Content $inner.Content) } default { } } } return $result } |