Closed ray-wn closed 1 week ago
A PFX certificates is PKCS12 format data which is ASN.1 which in turn is binary. There should be no character set involved because there is nothing to translate. The Base64 data was text, when you decode Base64 you have an octet-stream or byte array. Your mime type looks correct, what is the charset for?
A PFX certificates is PKCS12 format data which is ASN.1 which in turn is binary. There should be no character set involved because there is nothing to translate. The Base64 data was text, when you decode Base64 you have an octet-stream or byte array. Your mime type looks correct, what is the charset for?
I pass the file data like:
$boundary = 'someBoundary'
$body = (
"--$boundary",
"Content-Disposition: form-data; name=`"PWD`"$LF",
$certPw,
"--$boundary",
"Content-Disposition: form-data; name=`"FILE`"; filename=`"certificate.pfx`"",
"Content-Type: application/x-pkcs12$LF",
[System.Text.Encoding]::Latin1.GetString($bytes),
"--$boundary--$LF"
) -join $LF
$response = Invoke-RestMethod -ContentType "multipart/form-data; boundary=$boundary" -Uri $certupload_uri -Method Post -Body $body -WebSession $session -ErrorAction Stop
...of course I also tried the same with $mulitpart = [System.Net.Http.MultipartFormDataContent]::new()
I mean, when I see the sent form-data it's now UTF-8 encoded, which doesn't work, so setting it to Latin1 should correctly send the data with ISO-8895-1 encoding. As mentioned, that's also what happens when I manually upload it. For the other devices setting charset=iso-8895-1 correctly POSTed the data with that encoding and in Fiddler I see the content is correctly encoded (ANSI, when viewing in Notepad) which is obviously not the case with 7.4, as it defaults to UTF-8.
Edit: Here the multipart version, same issue
$bytes = [System.Convert]::FromBase64String('MII....=')
$FileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data')
$FileHeader.Name = 'FILE'
$FileHeader.FileName = 'certificate.pfx'
$FileContent = [System.Net.Http.StreamContent]::new([System.IO.MemoryStream]::new($bytes))
$FileContent.Headers.ContentDisposition = $FileHeader
$FileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/x-pkcs12')
$MultipartContent = [System.Net.Http.MultipartFormDataContent]::new()
$MultipartContent.Add([System.Net.Http.StringContent]::new($certPw), 'PWD')
$MultipartContent.Add($FileContent)
So, charset to send the body data as Latin1.
I must admit I am not familiar with multipart/form-data
This looks wrong
[System.Text.Encoding]::Latin1.GetString($bytes)
The bytes are bytes and should not be converted, they should stay as bytes. I don't know how the length of the mime data within each part is encoded.
My example
$ od -t x1 random.bin
0000000 0f cc eb 1c bc b7 30 8f aa aa bc 40 77 d4 a7 65
0000020 9c 5a fb 35 ee b2 1e bf af 89 ea 58 94 8d 5b 2b
0000040
$ curl --trace - --user guest:changeit --form "file=@random.bin" --fail http://localhost:8080
== Info: Trying 127.0.0.1:8080...
== Info: Connected to localhost (127.0.0.1) port 8080 (#0)
== Info: Server auth using Basic with user 'guest'
=> Send header, 229 bytes (0xe5)
0000: 50 4f 53 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d POST / HTTP/1.1.
0010: 0a 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 .Host: localhost
0020: 3a 38 30 38 30 0d 0a 41 75 74 68 6f 72 69 7a 61 :8080..Authoriza
0030: 74 69 6f 6e 3a 20 42 61 73 69 63 20 5a 33 56 6c tion: Basic Z3Vl
0040: 63 33 51 36 59 32 68 68 62 6d 64 6c 61 58 51 3d c3Q6Y2hhbmdlaXQ=
0050: 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 63 75 ..User-Agent: cu
0060: 72 6c 2f 37 2e 38 31 2e 30 0d 0a 41 63 63 65 70 rl/7.81.0..Accep
0070: 74 3a 20 2a 2f 2a 0d 0a 43 6f 6e 74 65 6e 74 2d t: */*..Content-
0080: 4c 65 6e 67 74 68 3a 20 32 33 34 0d 0a 43 6f 6e Length: 234..Con
0090: 74 65 6e 74 2d 54 79 70 65 3a 20 6d 75 6c 74 69 tent-Type: multi
00a0: 70 61 72 74 2f 66 6f 72 6d 2d 64 61 74 61 3b 20 part/form-data;
00b0: 62 6f 75 6e 64 61 72 79 3d 2d 2d 2d 2d 2d 2d 2d boundary=-------
00c0: 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d ----------------
00d0: 2d 62 39 38 64 33 38 39 63 64 62 34 62 35 32 64 -b98d389cdb4b52d
00e0: 32 0d 0a 0d 0a 2....
=> Send data, 234 bytes (0xea)
0000: 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d ----------------
0010: 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 62 39 38 64 33 38 ----------b98d38
0020: 39 63 64 62 34 62 35 32 64 32 0d 0a 43 6f 6e 74 9cdb4b52d2..Cont
0030: 65 6e 74 2d 44 69 73 70 6f 73 69 74 69 6f 6e 3a ent-Disposition:
0040: 20 66 6f 72 6d 2d 64 61 74 61 3b 20 6e 61 6d 65 form-data; name
0050: 3d 22 66 69 6c 65 22 3b 20 66 69 6c 65 6e 61 6d ="file"; filenam
0060: 65 3d 22 72 61 6e 64 6f 6d 2e 62 69 6e 22 0d 0a e="random.bin"..
0070: 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 61 70 Content-Type: ap
0080: 70 6c 69 63 61 74 69 6f 6e 2f 6f 63 74 65 74 2d plication/octet-
0090: 73 74 72 65 61 6d 0d 0a 0d 0a 0f cc eb 1c bc b7 stream..........
00a0: 30 8f aa aa bc 40 77 d4 a7 65 9c 5a fb 35 ee b2 0....@w..e.Z.5..
00b0: 1e bf af 89 ea 58 94 8d 5b 2b 0d 0a 2d 2d 2d 2d .....X..[+..----
00c0: 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d ----------------
00d0: 2d 2d 2d 2d 2d 2d 62 39 38 64 33 38 39 63 64 62 ------b98d389cdb
00e0: 34 62 35 32 64 32 2d 2d 0d 0a 4b52d2--..
I can see the 32 bytes of my file random.bin in the content from 009A to 00B9 but I don't know how it encodes that there are 32 bytes there. I can see that where is a boundary immediately following.
Given you are trying to pass binary data I don't think you can create the body with string appends. I think you may need something like MultipartFormDataContent Class
Eg how-to-send-multipart-form-data-with-powershell-invoke-restmethod
This is my PowerShell version using .NET
param(
[parameter(Mandatory=$true)]
$Path
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
trap
{
throw $PSItem
}
if ( Test-Path -LiteralPath $Path -PathType Leaf )
{
$web = New-Object -Type 'System.Net.WebClient'
try
{
$web.Headers["Authorization"] = "Basic Z3Vlc3Q6Y2hhbmdlaXQ="
$response = $web.UploadFile('http://localhost:8080',$Path)
}
finally
{
$web.Dispose()
}
}
else
{
Write-Host "Not uploading $Path as it does not exist"
}
https://github.com/PowerShell/PowerShell/issues/21386#issuecomment-2023863985 Has an example of building form data for Invoke-WebRequest, specifically with raw file bytes.
Thank you both for the responses and examples!
it expects the bytes to be Latin1.
The end system doesn't expect the bytes to be Latin1, it expects them to just be bytes. Raw bytes have no character encoding, they are just eight bits. Character-set encoding only applies to text. What has changed is that PowerShell's default character set is now UTF8, and you may have previously been lucky with the encoding and it was a one to one mapping, but really there should be no mapping at all. I suggest following the link above provided by @jborean93
Thanks for the clarification, that clears that part up for me! What that really means is that the form-data submitted needs to be Latin1 (and the file bytes within the form-data has nothing to do with the issue). The issue still stands but doesn't specifically relate to the .pfx bytes.
Note that when base64 decoding the bytes and saving the file with UTF-8, uploading the .pfx manually doesn't work, this is probably because then Chrome will see the file was saved with UTF-8 encoding and post the form-data with UTF-8 encoding so my confusion was that I thought it had to do with the bytes but it has to do with the form-data itself, regardless of the bytes.
What I still don't understand is what to do with the link, when I wrote in my first answer that it's the exact same issue, the device returns a JSON with the exact same error code. Here my code again, as in the first answer, using mulitpart-form with just bytes, exact same issue. Maybe I should delete the Latin1 part from my answers and just post the multipart-form version, as the issue is I can't send charset but the body may not be UTF-8 encoded:
$bytes = [System.Convert]::FromBase64String('MII....=')
$FileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data')
$FileHeader.Name = 'FILE'
$FileHeader.FileName = 'certificate.pfx'
$FileContent = [System.Net.Http.StreamContent]::new([System.IO.MemoryStream]::new($bytes))
$FileContent.Headers.ContentDisposition = $FileHeader
$FileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/x-pkcs12')
$MultipartContent = [System.Net.Http.MultipartFormDataContent]::new()
$MultipartContent.Add([System.Net.Http.StringContent]::new($certPw), 'PWD')
$MultipartContent.Add($FileContent)
Invoke-RestMethod -Body $MultipartContent ....
If I was going to try and solve this, I would get to the basics.
Can you do the equivalent with curl ( I mean real curl, not curl the PowerShell 5.1 alias)?
Something like
Set-Content -LiteralPath 'certificate.pfx' -Value $bytes -AsByteStream
curl --trace - --user XXX:YYYY --form "file=@certificate.pfx" --fail http://yoururl
Can you use Invoke-WebRequest to do the post to make sure the Cmdlet is not doing any extra translation of the data body?
Thanks a lot for all your help so far!
I will definitely try your suggestions, but it'll be on Monday, and comment. I'll try the MultipartForm version too, but am certain that'll work with 7.3. I'll definitely try the Invoke-WebRequest and see about curl, although I don't know curl very well and I will need to pass session data after logging in to the device.
I notice that the MultipartFormDataContent has a mechanism to convert the whole encoding to a byte stream, I think you need to be sending those bytes exactly as is through Invoke-WebRequest.
I haven't followed this thread in detail, so there may be a better solution, but note that you can pass a [byte[]]
array directly to the -Body
argument, so to emulate the 7.3- behavior you can try to ISO-8859-1-encode the entire $body
string:
# Create the $body text as an ISO-8859-1-encoded [byte[]] array.
$body = [System.Text.Encoding]::Latin1.GetBytes((
"--$boundary",
"Content-Disposition: form-data; name=`"PWD`"`n",
$certPw,
"--$boundary",
"Content-Disposition: form-data; name=`"FILE`"; filename=`"certificate.pfx`"",
"Content-Type: application/x-pkcs12`n",
[System.Text.Encoding]::Latin1.GetString($bytes),
"--$boundary--`n"
) -join "`n")
Note that any single-byte, 8-bit, fixed-width text encoding (all of whose code points must be valid) can be used to losslessly encode arbitrary binary data.
ISO-8859-1 encoding (which is closely related to - but not the same as - Windows-1252) is special in that it is a true subset of Unicode, covering the 8-bit range of Unicode code points (�
(NULL, U+0000
) through ÿ
(LATIN SMALL LETTER Y WITH DIAERESIS, U+00FF
)
As such, you can cast arbitrary binary data to (the characters of) a .NET string, and the Unicode code points of the resulting characters (which are technically are Unicode code units, i.e. [uint16]
values (unsigned 16-bit integers) that situationally, but rarely, require a complementary, second code unit, with two such code units forming a so-called surrogate pair) will reflect the original byte values.
# Arbitrary binary data (array of bytes)
[byte[]] $bytes = 0..255
# Interpret the byte values as Unicode code units ([char]) and construct a .NET string
# from them (which is always UTF-16LE).
$str = [string] [char[]] $bytes
Because of the relationship between Unicode and ISO-8859-1, $str = [string] [char[]] $bytes
is the same as $str = [System.Text.Encoding]::Latin1.GetString($bytes)
Conversely, [System.Text.Encoding]::Latin1.GetBytes($str)
is the same as [byte[]] [char[]] $str
and yields the original binary data (but note that not all .NET strings can be converted this way, namely if they contain characters with Unicode code points >= 256, such as €
: the former expression would "lossily" transliterate such characters to verbatim ?
characters, the latter would report an error - with byte values as the origin, that is by definition not a concern, however).
This is my solution, you need $path and $uri set up, My credentials are just as an example. This has no text or character sets. Everything is done using System.Net.Http objects to do the correct encoding.
Set up credentials
[string]$userName = 'guest'
[string]$userPassword = 'changeit'
[securestring]$secStringPassword = ConvertTo-SecureString $userPassword -AsPlainText -Force
[pscredential]$credObject = New-Object System.Management.Automation.PSCredential -ArgumentList $userName, $secStringPassword
Read file into memory and get name and content type
[byte[]]$bytes = Get-Content -LiteralPath $Path -AsByteStream -Raw
$name = (Get-Item -LiteralPath $Path).Name
[string]$contentType = 'application/octet-stream'
Create the multipart body
$byteArrayContent = New-Object -TypeName 'System.Net.Http.ByteArrayContent' -ArgumentList @(,$bytes)
$byteArrayContent.Headers.ContentType = New-Object -TypeName 'System.Net.Http.Headers.MediaTypeHeaderValue' -ArgumentList $contentType
$multipartFormDataContent = New-Object -TypeName 'System.Net.Http.MultipartFormDataContent'
$multipartFormDataContent.Add($byteArrayContent,'FILE',$name)
Get bytes and contentType from the multipart content
$memory = New-Object -TypeName 'System.IO.MemoryStream'
$multipartFormDataContent.ReadAsStream().CopyTo($memory)
$bytes = $memory.ToArray()
$contentType = $multipartFormDataContent.Headers.ContentType
Finally do the post with Invoke-WebRequest
Invoke-WebRequest -Credential $credObject-Uri $Uri -Method 'POST' -Body $bytes -ContentType $contentType
In my case I get a 201 created or a 204 no content as a successful response.
Alternatively use Invoke-RestMethod
Invoke-RestMethod -Credential $credObject-Uri $Uri -Method 'POST' -Body $bytes -ContentType $contentType
You guys are the best! It worked!
@mklement0 It worked as you mentioned. I had to save the bytes to a variable first. For some reason I couldn't get doing the GetBytes within the Invoke-RestMethod to work, and actually tried that before too.
@rhubarb-geek-nz I tried quite a while but couldn't fully get it to work, but super helpful for using the MultipartFormDataContent. I tried to use your example with the solution of getting the bytes from Latin1. I think the issue is that to pass the body I just need the MultipartFormDataContent Content and not the headers etc., so not sure getting the bytes returns other stuff too? I tried removing the headers too, but that didn't fully work. I guess I could use the enumerator and then parse the bytes, but for now I'm just happy I have a solution.
Thank you both very very much!
📣 Hey @ray-wn, how did we do? We would love to hear your feedback with the link below! 🗣️
🔗 https://aka.ms/PSRepoFeedback
Glad to hear it helped, @ray-wn (I've updated my previous comment to amend the background information about encodings).
You don't strictly a need an intermediate variable, though, so I suspect you merely had a syntax problem, which is presumably easily remedied by enclosing the [System.Text.Encoding]::Latin1.GetBytes()
call in (...)
; a simplified example:
# Note the (...) around the [System.Text.Encoding]::Latin1.GetBytes() call.
Write-Host ([System.Text.Encoding]::Latin1.GetBytes('abc'))
In argument-parsing mode, [
is not a metacharacter; to force recognition of an expression such as a method call starting with [System.Text.Encoding]
as such, enclosure in (...)
is needed.
Thanks again @mklement0!!! You're absolutely right, I just retried it as I was certain that's what I did, but it worked. I have no idea what I must have done, as adding parentheses would be the first logical thing to do.
I tried to use your example with the solution of getting the bytes from Latin1.
I am glad you having something working and are happy with it.
I would still suggest that any solution that depends on encoding bytes with any character set and still expecting them to be treated as the correct bytes at the end of a chain is a 'hack'. Also the advantage of the MultipartFormDataContent is it handles all the boundaries.
For the record, this is my complete script which lets me transfer multiple files to my system in a single POST.
#!/usr/bin/env pwsh
param(
[parameter(Mandatory=$true)]$Path,
$Uri='http://wherever.it.goes'
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
trap
{
throw $PSItem
}
[string]$userName = 'guest'
[string]$userPassword = 'changeit'
[securestring]$secStringPassword = ConvertTo-SecureString $userPassword -AsPlainText -Force
[pscredential]$credObject = New-Object System.Management.Automation.PSCredential -ArgumentList $userName, $secStringPassword
$multipartFormDataContent = New-Object -TypeName 'System.Net.Http.MultipartFormDataContent'
Get-Item -Path $path | ForEach-Object {
if (Test-Path -Path $_ -Type Leaf)
{
$name = $_.Name
[byte[]]$bytes = Get-Content -LiteralPath $_ -AsByteStream -Raw
$byteArrayContent = New-Object -TypeName 'System.Net.Http.ByteArrayContent' -ArgumentList @(,$bytes)
$byteArrayContent.Headers.ContentType = New-Object -TypeName 'System.Net.Http.Headers.MediaTypeHeaderValue' -ArgumentList 'application/octet-stream'
$multipartFormDataContent.Add($byteArrayContent,'FILE',$name)
}
}
$memory = New-Object -TypeName 'System.IO.MemoryStream'
$multipartFormDataContent.ReadAsStream().CopyTo($memory)
$bytes = $memory.ToArray()
$contentType = $multipartFormDataContent.Headers.ContentType
Invoke-RestMethod -Credential $credObject -AllowUnencryptedAuthentication -Uri $Uri -Method 'POST' -Body $bytes -ContentType $contentType
@rhubarb-geek-nz You're totally right and I agree that this is the cleanest way. Right now I have other issues to resolve, but will definitely revisit it when I have a bit more time to look into it, but thanks a lot for the code and all your help!
Prerequisites
Steps to reproduce
I have tried several attempts over countless hours, and nothing seems to work. I've also read dozens of Issues (open and closed).
I'm uploading a multipart/form-data certificate (.pfx) from a decoded Base64 Latin1/ISO-8895-1 string. Setting charset just returns an error code. I've had the same issue with another device but setting -ContentType="application/x-pkcs12;boundary=someBoundary;charset=iso-8895-1" worked. but for this device nothing works. Can't upload it as UTF-8 and can't pass charset. I guess e.g. Chrome detects the form-data/file's encoding and submits it accordingly or it uses ISO-8859-1/ASCII.
Edit (May 4): It has to do with the form-data body being submitted as UTF-8 (and not about the .pfx bytes' encoding).
I don't have access to modify the device and I don't see any way to force the old pwsh behavior.
Expected behavior
Actual behavior
Error details
No response
Environment data
Visuals
No response