Closed jeff-simeon closed 3 years ago
It should be possible, you essentially need to ensure the libmi
and libpsrpclient
in the pwsh dir are replaced. The Install-WSMan
cmdlet distributed in PSWSMan
does this for you but there's nothing stopping you from doing it yourself. You could also add an ImportResolver
as mentioned in https://github.com/jborean93/omi/issues/24#issuecomment-798995921 to ensure libpsrpclient
loads the so/dylib distributed by PSWSMan instead of replacing what PowerShell ships with.
Thanks. I tried this and didn't make it too far.
I added this to my csproj:
<ItemGroup>
<Content Include="PSWSMan/2.2.1/lib/macOS-3/*.*" CopyToOutputDirectory="Always" Link="%(Filename)%(Extension)" />
</ItemGroup>
which gets the dylib's copied to my bin directory
Added this to my entry point
System.Runtime.InteropServices.NativeLibrary.SetDllImportResolver(typeof(PSObject).Assembly, ImportResolver);
and validated my resolver gets called:
public static IntPtr ImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName == "libpsrpclient" || libraryName == "libmi") {
libraryName = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), $"{libraryName}.dylib");
}
return NativeLibrary.Load(libraryName, assembly, searchPath);
}
Unfortunately, this blows up at
return NativeLibrary.Load(libraryName, assembly, searchPath);
with the exception
An exception of type 'System.DllNotFoundException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Unable to load shared library '/Users/jeff/dev/PowerShellWSManBug/bin/Debug/net5.0/libpsrpclient.dylib' or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable: dlopen(/Users/jeff/dev/PowerShellWSManBug/bin/Debug/net5.0/libpsrpclient.dylib, 1): Library not loaded: @loader_path/libssl.3.dylib
Referenced from: /Users/jeff/dev/PowerShellWSManBug/bin/Debug/net5.0/libmi.dylib
Reason: image not found'
at System.Runtime.InteropServices.NativeLibrary.LoadByName(String libraryName, QCallAssembly callingAssembly, Boolean hasDllImportSearchPathFlag, UInt32 dllImportSearchPathFlag, Boolean throwOnError)
the file is, of course, there
The number in the distribution refers to the OpenSSL version it is linked against. Because each OpenSSL major version is not ABI (and sometimes API) compatible the version you use must reflect what OpenSSL version you have available. You most likely have OpenSSL 1.1.x installed with brew (3 is still in the release candidate phase) so you should include macOS-1.1
.
MacOS also makes things a bit more difficult as it's trying to find libssl
and libcrypto
relative to libmi.dylib
(@loader_path/libssl.1.1.dylib
). The Install-WSMan
cmdlet will create the 2 symlinks for these files in the same dir as libmi.dylib
to point to the actual libssl
and libcrypto
either provided by brew
or port
. You will either:
install_name_tool
to adjust the libssl
and libcrypto
entries in libmi.dylib
to the location on your host# Must be done for libssl and libcrypto. The target path is dependent on your host in question
# For brew see 'echo "$( brew --prefix openssl )/lib"'
# For port see 'port contents openssl'
install_name_tool -change \
@loader_path/libssl.1.1.dylib \
/usr/local/opt/openssl@1.1/lib/libssl.dylib \
libmi.dylib
# You can verify the paths with 'otool -L libmi.dylib'
It's a mess unfortunately but unfortunately I don't know of a nice way to achieve all this that is universal across macOS distributions. The proper solution is to adjust the code so it uses Apple's native TLS library, Network, but that's a lot more than I can chew at the moment. I don't even think it's possible to use the BIO memory like setup that OpenSSL offers which makes using that even harder for this library.
if (libraryName == "libpsrpclient" || libraryName == "libmi") {
As an FYI you only need to adjust libpsrpclient
for the .NET resolver. PowerShell imports libpsrpclient
which in turn imports libmi
through the dylib loading process completely outside of .NET.
Edit: Changed install_name_tool argument order.
Thanks!
I must have done something wrong...I install openssl via MacPorts (it's not available on Brew anymore that I can see).
I grabbed the path of my install
Jeffs-MBP ~ % port contents openssl | grep dylib
/opt/local/lib/engines-1.1/capi.dylib
/opt/local/lib/engines-1.1/padlock.dylib
/opt/local/lib/libcrypto.1.1.dylib
/opt/local/lib/libcrypto.dylib
/opt/local/lib/libssl.1.1.dylib
/opt/local/lib/libssl.dylib
I switched to 1.1
<ItemGroup>
<Content Include="PSWSMan/2.2.1/lib/macOS-1.1/*.*" CopyToOutputDirectory="Always" Link="%(Filename)%(Extension)" />
</ItemGroup>
and I ran this command from my bin/Debug/net5.0 directory
net5.0 % sudo install_name_tool -change \
libmi.dylib \
@loader_path/libssl.1.1.dylib \
/opt/local/lib/libssl.1.1.dylib
But I still get the error
An exception of type 'System.DllNotFoundException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Unable to load shared library '/Users/jeff/dev/PowerShellWSManBug/bin/Debug/net5.0/libpsrpclient.dylib' or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable: dlopen(/Users/jeff/dev/PowerShellWSManBug/bin/Debug/net5.0/libpsrpclient.dylib, 1): Library not loaded: @loader_path/libssl.1.1.dylib
Hmm based on the error it is still trying to find libssl
using @loader_path/libssl.1.1.dylib
which shouldn't be the case after running install_name_tool
. Can you run otool -L libpsrpclient.dylib
and otool -L libmi.dylib
and share those results? You shouldn't have to run install_name_tool
as root as well. It just edits the ELF headers of the dylib specified so as long as you have rights to the file you are good to go.
Ah sorry I gave you the wrong order of arguments it should be
install_name_tool -change \
@loader_path/libssl.1.1.dylib \
/opt/local/lib/libssl.1.1.dylib \
libmi.dylib
The first argument is what to change, the 2nd is what to change it to, and the 3rd is the library to change.
Progress!
An unhandled exception of type 'System.AggregateException' occurred in System.Private.CoreLib.dll: 'One or more errors occurred.'
Inner exceptions found, see $exception in variables window for more details.
Innermost exception System.Management.Automation.ActionPreferenceStopException : The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: [outlook.office365.com] Connecting to remote server outlook.office365.com failed with the following error message : error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed For more information, see the about_Remote_Troubleshooting Help topic.
Any idea why certificate validation would be failing? Needless to say, I checked in a regular PowerShell command prompt and didn't get a failure.
Sweet seems like the library is actually being loaded and used so we moving forward. I'm not sure why the certificate is not being verified as the cert MS is using for that site should be valid. I have a few questions:
openssl
that was installed with port to ensure you replicate the same engine that OMI will be usingecho "q" | /opt/local/bin/openssl s_client -connect outlook.office365.com:443 -showcerts
Jeffs-MBP bin % echo "q" | /opt/local/bin/openssl s_client -connect outlook.office365.com:443 -showcerts
Server certificate subject=C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = outlook.com
issuer=C = US, O = DigiCert Inc, CN = DigiCert Cloud Services CA-1
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384 Server public key is 2048 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-RSA-AES256-GCM-SHA384 Session-ID: 8116FFAE39C9C852D260E258E9296216B2DFC84DF303672FEBBAD42B862BB723 Session-ID-ctx: Master-Key: 38B444EE9B97ABD881B57F84CE514AD51B00656220E3C6EF5FC27A1DCDC71006F3AC9ECFB944C06A2BC0E912193A0E82 PSK identity: None PSK identity hint: None SRP username: None TLS session ticket lifetime hint: 36000 (seconds) TLS session ticket: 0000 - 00 00 00 00 9f d2 90 4b-75 94 47 4d ae 48 99 c6 .......Ku.GM.H.. 0010 - 73 21 62 27 5f 7c 82 f3-c0 37 22 45 98 6d ea 89 s!b'_|...7"E.m.. 0020 - 0a 28 c3 56 65 f7 15 36-f1 1d a1 f7 27 ea 5a a9 .(.Ve..6....'.Z. 0030 - ad 13 3e 68 07 19 9a 05-8d df 99 2a 30 26 42 61 ..>h.......*0&Ba 0040 - 59 46 18 7f ae 14 15 c5-f8 25 05 d6 48 cb 46 90 YF.......%..H.F. 0050 - 46 46 7c 77 82 9e 6c 8a-e2 e1 5d 53 ef 0f 17 ab FF|w..l...]S.... 0060 - e4 39 7d f9 4b 17 73 83-0b 24 49 b1 cc 89 49 f3 .9}.K.s..$I...I. 0070 - 97 8b 00 d0 32 85 d7 e5-15 34 6b 7d d1 c4 19 b2 ....2....4k}.... 0080 - fc 30 9e d5 20 20 d9 7d-3c 37 86 40 d3 06 20 ee .0.. .}<7.@.. . 0090 - 30 05 99 aa 31 3b d0 c3-4f b7 c5 53 aa 09 f1 df 0...1;..O..S.... 00a0 - 45 b0 20 2f 65 f7 b5 80-2d 63 0f db 68 fb cf 5d E. /e...-c..h..] 00b0 - 62 b9 94 76 30 69 0b 7a-17 9b dc bc c5 9e 12 c1 b..v0i.z........ 00c0 - e2 14 86 52 30 e2 43 56-68 d7 45 f2 0d f8 c0 2d ...R0.CVh.E....- 00d0 - aa c6 9e a3 eb b0 57 e2-10 f8 a5 f8 5a 92 60 d3 ......W.....Z.`. 00e0 - 3a 1e 20 34 af 8a e3 59-26 dd 0a 77 89 3e 78 24 :. 4...Y&..w.>x$ 00f0 - 1a b3 8d b8 33 68 05 e5-0a 2b 46 bf 73 f8 9d a4 ....3h...+F.s... 0100 - 9c cf 2f 3f ../?
Start Time: 1630005415
Timeout : 7200 (sec)
Verify return code: 20 (unable to get local issuer certificate)
Extended master secret: yes
- The default openssl on the system produces slightly different output:
Jeffs-MBP bin % echo "q" | /opt/local/bin/openssl s_client -connect outlook.office365.com:443 -showcerts
Server certificate subject=C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = outlook.com
issuer=C = US, O = DigiCert Inc, CN = DigiCert Cloud Services CA-1
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384 Server public key is 2048 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-RSA-AES256-GCM-SHA384 Session-ID: 184FAACAB6BE93B65195FCA04AE62754D41283DEF6330B6E45C6848ABA03732A Session-ID-ctx: Master-Key: F07DAFD277C26391AF4A1F37A2B4F94B8F3FECAF77EAD1CC02FE5F75F1E14861D6ACD36E88721391DB249C1B81FDAFEF PSK identity: None PSK identity hint: None SRP username: None TLS session ticket lifetime hint: 36000 (seconds) TLS session ticket: 0000 - 00 00 00 00 9f d2 90 4b-75 94 47 4d ae 48 99 c6 .......Ku.GM.H.. 0010 - 73 21 62 27 36 33 b1 81-31 f3 40 1b 7e b4 d4 80 s!b'63..1.@.~... 0020 - b4 78 d5 8e 5e 27 de 4c-ac 8b d4 04 dd 8f 1d d1 .x..^'.L........ 0030 - f4 1e de 97 a7 e1 5d d0-6f 2e b3 d0 42 8e 05 b2 ......].o...B... 0040 - 5e 49 9e 0a b2 c9 79 a5-35 1e 3b f5 9f 9d 8a aa ^I....y.5.;..... 0050 - f7 a5 81 06 7a a2 33 5a-7c c2 20 1f 33 0f cf 62 ....z.3Z|. .3..b 0060 - d2 d6 67 eb 46 2b f8 d5-4e cd 24 15 6f be 5f 7d ..g.F+..N.$.o._} 0070 - 84 d8 6b 96 a2 d9 55 9a-b2 6b 76 6e 17 02 67 10 ..k...U..kvn..g. 0080 - c8 b1 03 ec 43 d0 40 68-66 63 fc 70 ce 79 dd 7a ....C.@hfc.p.y.z 0090 - 21 c1 78 85 bb 94 c9 ec-6b 6b 22 65 0c 77 ad f3 !.x.....kk"e.w.. 00a0 - e0 6c 91 0a 48 49 70 7a-cf 84 37 dd e2 9c 13 82 .l..HIpz..7..... 00b0 - e0 bb fc e7 f4 ca a2 af-cb f3 58 9a 14 02 f4 a0 ..........X..... 00c0 - 6d 69 31 0f d1 07 2b 45-6b 1e c0 9c 5b 83 b4 19 mi1...+Ek...[... 00d0 - cf 07 27 48 7c de 1b 87-10 2b b0 66 46 22 b0 c7 ..'H|....+.fF".. 00e0 - 99 a0 13 36 92 e8 68 f5-db 4e 2d e3 ae 84 b4 da ...6..h..N-..... 00f0 - 17 47 ee 90 0e 29 fd b0-79 ec ae 46 d1 d8 3e a4 .G...)..y..F..>. 0100 - 41 6a c2 1f Aj..
Start Time: 1630005474
Timeout : 7200 (sec)
Verify return code: 20 (unable to get local issuer certificate)
Extended master secret: yes
DONE
The default openssl on the system produces slightly different output:
Looks like you used the same OpenSSL for both outputs (/opt/local/bin/openssl
). Nevertheless you need to resolve this problem so that OMI can verify the certificate. I have no idea why it cannot verify the issuer of C = US, O = DigiCert Inc, CN = DigiCert Cloud Services CA-1
, potentially the root certs for OpenSSL installed with port isn't complete. I know with brew you have to run a separate command to sync the CA certificates with the OS keychain so potentially port
is the same. You will have to read up on their documentation, a quick google search brings up packages like certsync
or curl-ca-bundle
(https://andatche.com/articles/2012/02/fixing-ssl-ca-certificates-with-openssl-from-macports/).
You can disable cert verification by using the env vars OMI_SKIP_CA_CHECK
and OMI_SKIP_CN_CHECK
but if you are setting them at runtime in your C# app you need to do it in a special way and not through the .NET API. See https://github.com/jborean93/omi/blob/main/docs/https_validation.md for more details. This is really not recommended as you should be relying on the cert verification of HTTPS to ensure the host you are talking to is who it says it is. Luckily it sounds like you just need to sync the CA certificates for the port OpenSSL and you will be in business.
I have a full example here: https://github.com/jeff-simeon/PowerShellWSManBug (no WSManConnectionInfo)
Is there a reason why you are targeting <PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.3" />
. You should really be using something in the 7.x releases as 6 is EOL https://docs.microsoft.com/en-us/powershell/scripting/powershell-support-lifecycle?view=powershell-7.1.
I created that example a year and a half ago :)
That would make a lot of sense then :)
Thanks - Using
sudo port install curl-ca-bundle
resolved the SSL issue! Should have figured that one out myself.
Next error:
An unhandled exception of type 'System.AggregateException' occurred in System.Private.CoreLib.dll: 'One or more errors occurred.'
Inner exceptions found, see $exception in variables window for more details.
Innermost exception System.Management.Automation.ActionPreferenceStopException : The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: [outlook.office365.com] Connecting to remote server outlook.office365.com failed with the following error message : Basic Authorization failed for user **** For more information, see the about_Remote_Troubleshooting Help topic.
What's odd here is that I'm using Bearer Auth, not basic. However, the way the remote session works is it takes a Basic Auth header with a user id and bearer token. Any idea what I need to do to get bearer auth working? Note that this works when running the same PowerShell code from pwsh.
To WSMan the modern auth is essentially basic auth, it’s the service that interprets it differently. Have a look at https://gist.github.com/jborean93/d50041c2a0fed20e87aa46ba32381754#file-new-exopssession-ps1-L176-L188 which implements modern auth in New-PSSession. Keep in mind the URI has a new prefix and the format of the username and password is quite particular.
I’m unsure why it would fail through the SDK but not when done on a pwsh instance but see if you can find any differences between your script and that snippet.
Got it working! Thank you!
Just FYI - as an experiment - I tried using the same install_name_tool
commands on bin/runtimes/osx/native/libmi.dylib' and it didn't work (still broke with the original
image not found` error)...so clearly it's not just an OpenSSL path issue.
I'm down to one final issue it seems. This isn't a problem with anything related to your OMI fork, but if you have any knowledge on this, would certainly appreciate the help. Exchange Online works fine, but the following code for Security Center works perfectly from Windows (both powershell and pwsh), but bombs on Mac with the error (exact same code with the exact same token...just copy/pasted from Windows to Mac):
Connecting to remote server ps.compliance.protection.outlook.com failed with the following error message : Basic Authorization failed for user
.
On Mac, this fails both using pwsh and SDK and both with the default libmi as well as the one from PSWSMan.
$global:InformationPreference = 'Continue'
$global:ErrorActionPreference = 'Stop'
$global:ProgressPreference = 'SilentlyContinue'
function Get-Session {
Get-PSSession |? ConfigurationName -eq 'Microsoft.Exchange'
}
function Remove-Session {
foreach($s in Get-Session) {
try {
Write-Information "Removing existing open PSSession $($s.Id) for Microsoft.Exchange at https://ps.compliance.protection.outlook.com/powershell-liveid/?BasicAuthToOAuthConversion=true."
$s | Remove-PSSession
}
catch {
Write-Warning "Encountered an error removing PSSession $($s.Id) ($($_.Exception.Message))."
}
}
}
function Initialize-Session {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$userCredential = New-Object System.Management.Automation.PSCredential('***', (ConvertTo-SecureString 'Bearer ***' -AsPlainText -Force))
$option = New-PSSessionOption
$option.IdleTimeout = [TimeSpan]::FromSeconds(60) # inline setting of this property via New-PSSessionOption is not supported on non-Windows platforms
$option.OpenTimeout = [TimeSpan]::FromSeconds(60)
Write-Information 'Creating PSSession for Microsoft.Exchange at https://ps.compliance.protection.outlook.com/powershell-liveid/?BasicAuthToOAuthConversion=true.'
$connectionUri = 'https://ps.compliance.protection.outlook.com/powershell-liveid?BasicAuthToOAuthConversion=true'
if ($IsMacOS) {
# https://github.com/PowerShell/PowerShell/issues/7276
$connectionUri = $connectionUri.Replace('&', '&')
}
Write-Host $connectionUri
$session = New-PSSession -SessionOption $option -ConfigurationName Microsoft.Exchange -ConnectionUri $connectionUri -Credential $userCredential -Authentication Basic -AllowRedirection -WarningAction SilentlyContinue
$module = Import-PSSession $session -DisableNameChecking -AllowClobber -WarningAction SilentlyContinue
Import-Module $module -Global -WarningAction SilentlyContinue
Write-Information "Imported PSSession $($session.Id) and Module for Microsoft.Exchange at https://ps.compliance.protection.outlook.com/powershell-liveid/?BasicAuthToOAuthConversion=true in $($sw.ElapsedMilliseconds)ms."
}
Remove-Session
Initialize-Session
Small update - I tried using Fiddler Anywhere to compare the activity on Windows vs Mac and unfortunately, it doesn't look like PowerShell Core on Mac supports the notion of proxies for WinRM, so I can't get Fiddler to intercept the traffic on Mac...so I'm kind of stuck poking in the dark.
I'm unsure really, modern/basic auth is really quite simple from OMI's perspective. It's adding the base64 encoded value you specify so there's really no platform specific logic at play here. I would just make sure the method you use to retrieve the bearer token is the same across the platform and the value it is has a similar structure. I would also remove the $connectionUri = $connectionUri.Replace('&', '&')
and see if it's actually required anymore.
As for proxying you might have some luck setting the env var https_proxy=http://my-fidler-host
as you start your app as it might just use that by default.
$connectionUri = $connectionUri.Replace('&', '&')
yes - tried this as well - it definitely IS required for Exchange Online because EXO required the extra query string parameter DelegatedOrg which SC does not...so with the SC config it is a no-op because there is no & in the query string
https_proxy=http://my-fidler-host
yep - already tried that to no avail :(
what's so strange is that it's literally the exact same code and tokens that work/don't work between Windows/Mac
My hunch is that there is something about the content of the XML in the POST requests that is making it lose the fact that it's an OAuth attempt vs a regular basic auth attempt.
Not sure what else that could be done, you can enable DEBUG logging for OMI and look at the raw messages there. It should also contain the headers that are sent https://github.com/jborean93/omi#troubleshooting. You probably want to create /opt/omi/var/log
which is where OMI places the trace files for the HTTP requests. I don't believe you can control this dir through the omicli.conf
but it's been a while since I last played with it.
I know I was definitely able to connect to EXO without replacing the values in the connection URI but I was using the Office 365 URL specified in https://gist.github.com/jborean93/d50041c2a0fed20e87aa46ba32381754#file-new-exopssession-ps1-L176-L188. Unfortunately at this point you are at the mercy of what OMI provides you and are beyond my knowledge sorry.
Thanks - I enabled the logs and that got me on the right track. It's the redirection...
AllowRedirection
in New-PSSession
seems to be broken for Mac.
If I use the connection URI
$connectionUri = 'https://nam10b.ps.compliance.protection.outlook.com/powershell-liveid/?BasicAuthToOAuthConversion=true'
it works (note the extra nam10b part of the hostname)
I'll see if I can get the redirected hostname manually using a REST call, unless you have an idea on the matter.
SUMMARY
PowerShell Core has abandoned support of OMI and PowerShell SDK can't be used for Exchange Online even with the symbolic link created to OpenSSL.
The issue is documented here https://github.com/PowerShell/PowerShell/issues/10600
with a minimally reproducible example here: https://github.com/JeffN825/PowerShellWSManBug
Is it possible to make this work using your fork?
Thanks! Great work on this project.
OS / ENVIRONMENT
Mac OS Big Sur