diff --git a/.github/workflows/psscriptanalyzer.yml b/.github/workflows/psscriptanalyzer.yml index 3f94b3f..7ea5f08 100644 --- a/.github/workflows/psscriptanalyzer.yml +++ b/.github/workflows/psscriptanalyzer.yml @@ -2,14 +2,22 @@ name: Run PSScriptAnalyzer on: push: + branches: [main, dev] + pull_request: branches: [main] +permissions: + security-events: write + contents: read + jobs: check: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Run PSScriptAnalyzer uses: microsoft/psscriptanalyzer-action@v1.1 @@ -19,6 +27,6 @@ jobs: output: results.sarif - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif diff --git a/DNSHealth/DNSHealth.psd1 b/DNSHealth/DNSHealth.psd1 index 1e969f2..1eb07d1 100644 --- a/DNSHealth/DNSHealth.psd1 +++ b/DNSHealth/DNSHealth.psd1 @@ -53,8 +53,14 @@ # Modules that must be imported into the global environment prior to importing this module #RequiredModules = @('') - # Assemblies that must be loaded prior to importing this module - # RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module. + # Preloads the precompiled SevenTinyRsa.dll (Source/SevenTinyRsa/) so + # Get-RsaPublicKeyInfo finds [SevenTiny.Bantina.Security.RSACommon] + # already registered — its existing Add-Type guard then short-circuits + # via the `-as [type]` check. Required for hosts where runtime + # `Add-Type -Language CSharp` fails (e.g. embedded-PowerShell hosts + # with no $PSHOME/ref reference-assemblies directory). + RequiredAssemblies = @('SevenTinyRsa.dll') # Script files (.ps1) that are run in the caller's environment prior to importing this module. ScriptsToProcess = @() diff --git a/DNSHealth/SevenTinyRsa.dll b/DNSHealth/SevenTinyRsa.dll new file mode 100644 index 0000000..1770c9c Binary files /dev/null and b/DNSHealth/SevenTinyRsa.dll differ diff --git a/Source/SevenTinyRsa/.gitignore b/Source/SevenTinyRsa/.gitignore new file mode 100644 index 0000000..9673e94 --- /dev/null +++ b/Source/SevenTinyRsa/.gitignore @@ -0,0 +1,4 @@ +# .NET build intermediates — the built DLL is committed under +# ../../DNSHealth/SevenTinyRsa.dll (see README.md). +bin/ +obj/ diff --git a/Source/SevenTinyRsa/README.md b/Source/SevenTinyRsa/README.md new file mode 100644 index 0000000..283d88c --- /dev/null +++ b/Source/SevenTinyRsa/README.md @@ -0,0 +1,44 @@ +# SevenTinyRsa + +Precompiled `[SevenTiny.Bantina.Security.RSACommon]` — the RSA public-key +parser that `Private/Get-RsaPublicKeyInfo.ps1` uses to decode DKIM keys. + +## Why this exists + +Upstream `Get-RsaPublicKeyInfo.ps1` ships the C# source inline and compiles +it at runtime via `Add-Type -TypeDefinition $source -Language CSharp`. That +works fine when DNSHealth is loaded by a normal `pwsh` process (which has +the .NET reference assemblies bundled at `$PSHOME/ref/`), but fails in any +host that embeds PowerShell as a library — `$PSHOME` resolves to the host's +own directory and Roslyn can't find the ref assemblies, so `Add-Type` +throws `DirectoryNotFoundException`. + +Bundling a precompiled DLL and listing it in `DNSHealth.psd1`'s +`RequiredAssemblies` registers the type before the module's runtime code +runs. `Get-RsaPublicKeyInfo`'s existing +`if (!('SevenTiny.Bantina.Security.RSACommon' -as [type]))` guard then +short-circuits and `Add-Type` is never reached. The inline source path +stays in place as a fallback for any future host that doesn't ship the +DLL. + +Behaviour on standard `pwsh` is unchanged — the type lookup just hits the +preloaded assembly instead of going through the runtime compile. + +## Rebuild + +```pwsh +cd Source/SevenTinyRsa +dotnet build SevenTinyRsa.csproj -c Release -o ../../DNSHealth/ +``` + +The output `SevenTinyRsa.dll` lands next to `DNSHealth.psd1`. +`build.psd1`'s `CopyDirectories` includes `SevenTinyRsa.dll`, so +`Build-Module` carries it into the built `Output/DNSHealth/` directory +beside the generated `.psm1`. + +## Provenance + +The C# source is verbatim from +[sevenTiny/Bamboo](https://github.com/sevenTiny/Bamboo) at +`10-Code/SevenTiny.Bantina/Security/RSACommon.cs`. The original +`Get-RsaPublicKeyInfo.ps1` cites the same source in its `.NOTES` block. diff --git a/Source/SevenTinyRsa/RSACommon.cs b/Source/SevenTinyRsa/RSACommon.cs new file mode 100644 index 0000000..a22d7f9 --- /dev/null +++ b/Source/SevenTinyRsa/RSACommon.cs @@ -0,0 +1,119 @@ +// Sourced verbatim from DNSHealth 1.1.8's vendored inline C# (DNSHealth.psm1 +// Private/Get-RsaPublicKeyInfo.ps1, lines 71-183 of the built .psm1). +// +// Origin: https://github.com/sevenTiny/Bamboo +// 10-Code/SevenTiny.Bantina/Security/RSACommon.cs + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace SevenTiny.Bantina.Security { + public static class RSACommon { + public static RSA CreateRsaProviderFromPublicKey(string publicKeyString) + { + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" + byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; + byte[] seq = new byte[15]; + + var x509Key = Convert.FromBase64String(publicKeyString); + + // --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------ + using (MemoryStream mem = new MemoryStream(x509Key)) + { + using (BinaryReader binr = new BinaryReader(mem)) //wrap Memory Stream with BinaryReader for easy reading + { + byte bt = 0; + ushort twobytes = 0; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + seq = binr.ReadBytes(15); //read the Sequence OID + if (!CompareBytearrays(seq, seqOid)) //make sure Sequence for OID is correct + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8203) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + bt = binr.ReadByte(); + if (bt != 0x00) //expect null byte next + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); + byte lowbyte = 0x00; + byte highbyte = 0x00; + + if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81) + lowbyte = binr.ReadByte(); // read next bytes which is bytes in modulus + else if (twobytes == 0x8202) + { + highbyte = binr.ReadByte(); //advance 2 bytes + lowbyte = binr.ReadByte(); + } + else + return null; + byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; //reverse byte order since asn.1 key uses big endian order + int modsize = BitConverter.ToInt32(modint, 0); + + int firstbyte = binr.PeekChar(); + if (firstbyte == 0x00) + { //if first byte (highest order) of modulus is zero, don't include it + binr.ReadByte(); //skip this null byte + modsize -= 1; //reduce modulus buffer size by 1 + } + + byte[] modulus = binr.ReadBytes(modsize); //read the modulus bytes + + if (binr.ReadByte() != 0x02) //expect an Integer for the exponent data + return null; + int expbytes = (int)binr.ReadByte(); // should only need one byte for actual exponent data (for all useful values) + byte[] exponent = binr.ReadBytes(expbytes); + + // ------- create RSACryptoServiceProvider instance and initialize with public key ----- + var rsa = System.Security.Cryptography.RSA.Create(); + RSAParameters rsaKeyInfo = new RSAParameters + { + Modulus = modulus, + Exponent = exponent + }; + rsa.ImportParameters(rsaKeyInfo); + + return rsa; + } + } + } + + private static bool CompareBytearrays(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + int i = 0; + foreach (byte c in a) + { + if (c != b[i]) + return false; + i++; + } + return true; + } + } +} diff --git a/Source/SevenTinyRsa/SevenTinyRsa.csproj b/Source/SevenTinyRsa/SevenTinyRsa.csproj new file mode 100644 index 0000000..8ac743b --- /dev/null +++ b/Source/SevenTinyRsa/SevenTinyRsa.csproj @@ -0,0 +1,17 @@ + + + net8.0 + SevenTinyRsa + SevenTiny.Bantina.Security + disable + latest + Library + true + none + false + false + false + bin\ + false + + diff --git a/build.psd1 b/build.psd1 index 6d82c35..2f13f6c 100644 --- a/build.psd1 +++ b/build.psd1 @@ -5,5 +5,11 @@ # Subsequent relative paths are to the ModuleManifest OutputDirectory = '..\Output\' VersionedOutputDirectory = $false - CopyDirectories = @('MailProviders') + # SevenTinyRsa.dll is the precompiled [SevenTiny.Bantina.Security.RSACommon] + # helper used by Get-RsaPublicKeyInfo. Bundling it lets DNSHealth.psd1's + # RequiredAssemblies preload the type so the inline Add-Type fallback + # short-circuits — required on hosts where runtime CSharp compilation + # fails (no $PSHOME/ref). Source: Source/SevenTinyRsa/. CopyDirectories + # is ModuleBuilder's alias for CopyPaths, which accepts files too. + CopyDirectories = @('MailProviders', 'SevenTinyRsa.dll') } \ No newline at end of file