How to use setCACert() of WiFiClientSecure with HTTPClient library? - esp8266

I'd like to set my CA root cert (currently available via WiFiClientSecure library) and use convenient HTTPClient library for making request.
How to do that? In the example there is only showed how to use WiFiClientSecure with manually written request.

It's way to late but this post is one one of the ones that gets returned by google so it's a good way to help others like me who are just starting out with this.
It turns out that all the posts about WifiClientSecure are obsolete (or simply unnecessary).
Here's a working example using the default HTTPClient. (not HttpClient)
It does require setting the certificate's sha1 fingerprint in the begin statement.
void loop() {
// wait for WiFi connection
if ((WiFiMulti.run() == WL_CONNECTED)) {
HTTPClient http;
Serial.print("[HTTPS] begin...\n");
http.begin("https://some.secure_server.com/auth/authorise", "2F 2A BB 23 6B 03 89 76 E6 4C B8 36 E4 A6 BF 84 3D DA D3 9F");
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int httpCode = http.POST("user_id=mylogin&user_password=this%20is%20my%20%24ecret%20pa%24%24word");
if (httpCode > 0) {
http.writeToStream(&Serial);
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTP] ... code: %d\n", httpCode);
// file found at server
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
}
} else {
Serial.printf("[HTTP] ... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
delay(10000);
}

Related

Dart TCP socket concatenates all 'write' sync calls as a single packet

I'm trying to send multiple packets at once to a server, but the socket keeps "merging" all sync calls to write as a single call, I did a minimal reproducible example:
import 'dart:io';
void main() async {
// <Server-side> Create server in the local network at port <any available port>.
final ServerSocket server =
await ServerSocket.bind(InternetAddress.anyIPv4, 0);
server.listen((Socket client) {
int i = 1;
client.map(String.fromCharCodes).listen((String message) {
print('Got a new message (${i++}): $message');
});
});
// <Client-side> Connects to the server.
final Socket socket = await Socket.connect('localhost', server.port);
socket.write('Hi World');
socket.write('Hello World');
}
The result is:
> dart example.dart
> Got a new message (1): Hi WorldHello World
What I expect is:
> dart example.dart
> Got a new message (1): Hi World
> Got a new message (2): Hello World
Unfortunately dart.dev doesn't support dart:io library, so you need to run in your machine to see it working.
But in summary:
It creates a new tcp server at a random port.
Then creates a socket that connects to the previous created server.
The socket makes 2 synchronous calls to the write method.
The server only receives 1 call, which is the 2 messages concatenated.
Do we have some way to receive each synchronous write call in the server as separated packets instead buffering all sync calls into a single packet?
What I've already tried:
Using socket.setOption(SocketOption.tcpNoDelay, true); right after Socket.connect instantiation, this does modify the result:
final Socket socket = await Socket.connect('localhost', server.port);
socket.setOption(SocketOption.tcpNoDelay, true);
// ...
Using socket.add('Hi World'.codeUnits); instead of socket.write(...), also does not modify the result as expected, because write(...) seems to be just a short version add(...):
socket.add('Hi World'.codeUnits);
socket.add('Hello World'.codeUnits);
Side note:
Adding an async delay to avoid calling write synchronously:
socket.add('Hi World'.codeUnits);
await Future<void>.delayed(const Duration(milliseconds: 100));
socket.add('Hello World'.codeUnits);
make it works, but I am pretty sure this is not the right solution, and this isn't what I wanted.
Environment:
Dart SDK version: 2.18.4 (stable) (Tue Nov 1 15:15:07 2022 +0000) on "windows_x64"
This is a Dart-only environment, there is no Flutter attached to the workspace.
As Jeremy said:
Programmers coding directly to the TCP API have to implement this logic themselves (e.g. by prepending a fixed-length message-byte-count field to each of their application-level messages, and adding logic to the receiving program to parse these byte-count fields, read in that many additional bytes, and then present those bytes together to the next level of logic).
So I chose to:
Prefix each message with a - and suffix with ..
Use base64 to encode the real message to avoid conflict between the message and the previously defined separators.
And using this approach, I got this implementation:
// Send packets:
socket.write('-${base64Encode("Hi World".codeUnits)}.');
socket.write('-${base64Encode("Hello World".codeUnits)}.');
And to parse the packets:
// Cache the previous parsed packet data.
String parsed = '';
void _handleCompletePacket(String rawPacket) {
// Decode the original message from base64 using [base64Decode].
// And convert the [List<int>] to [String].
final String message = String.fromCharCodes(base64Decode(rawPacket));
print(message);
}
void _handleServerPacket(List<int> rawPacket) {
final String packet = String.fromCharCodes(rawPacket);
final String next = parsed + packet;
final List<String> items = <String>[];
final List<String> tokens = next.split('');
for (int i = 0; i < tokens.length; i++) {
final String char = tokens[i];
if (char == '-') {
if (items.isNotEmpty) {
// malformatted packet.
items.clear();
continue;
}
items.add('');
continue;
} else if (char == '.') {
if (items.isEmpty) {
// malformatted packet.
items.clear();
continue;
}
_handleCompletePacket(items.removeLast());
continue;
} else {
if (items.isEmpty) {
// malformatted packet.
items.clear();
continue;
}
items.last = items.last + char;
continue;
}
}
if (items.isNotEmpty) {
// the last data of this packet was left incomplete.
// cache it to complete with the next packet.
parsed = items.last;
}
}
client.listen(_handleServerPacket);
There are certainly more optimized solutions/approaches, but I got this just for chatting messages within [100-500] characters, so that's fine for now.

Validate Apple StoreKit2 in-app purchase receipt jwsRepresentation in backend (node ideally, but anything works)

How can I validate an in-app purchase JWS Representation from StoreKit2 on my backend in Node?
Its easy enough to decode the payload, but I can't find public keys that Apple uses to sign these JWS/JWTs anywhere. Any other time I've worked with JWTs, you simply used the node jsonwebtoken library and passed in the signers public key or shared secret key, either configured or fetched from a JWK.
I can easily decode the JWS using node-jose j.JWS.createVerify().verify(jwsString, {allowEmbeddedKey: true}).then(r => obj = r) which gives me an object like:
{
protected: [ 'alg', 'x5c' ],
header: {
alg: 'ES256',
x5c: [
'MIIEMDueU3...',
'MII...,
'MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0...'
]
},
payload: <Buffer 7b 22 74 72 61 6e 73 61 63 74 69 6f 6e 49 64 22 3a 22 31 30 30 30 30 30 30 38 38 36 39 31 32 38 39 30 22 2c 22 6f 72 69 67 69 6e 61 6c 54 72 61 6e 73 ... 420 more bytes>,
signature: <Buffer f8 85 65 79 a1 dc 74 dd 90 80 0a a4 08 85 30 e7 22 80 4c 20 66 09 0b 84 fc f4 e5 57 53 da d5 6f 13 c6 8f 56 e8 29 67 5c 95 a6 27 33 47 1e fe e9 6e 41 ... 14 more bytes>,
key: JWKBaseKeyObject {
keystore: JWKStore {},
length: 256,
kty: 'EC',
kid: 'Prod ECC Mac App Store and iTunes Store Receipt Signing',
use: '',
alg: ''
}
}
And its easy to JSON.parse the payload and get the data I want. But, how can i verify that its authentic using the certificate chain in the x5c field
Thank you!
Finally figured this out. It turns out that we needed a "hardcoded" certificate to check against.
Apple has the certificates needed on their website. You have download the root certificate (since that's the one signing the entire chain), but you can also get the intermediate one.
Once you download one you convert it to .pem:
$ openssl x509 -inform der -in apple_root.cer -out apple_root.pem
then all you need to do is verify them against the ones in the JWS (the following is in PHP, but you should get the gist):
if (openssl_x509_verify($jws_root_cert, $downloaded_apple_root_cert) == 1){
//valid
}
Hope this helps everyone else!
It is quite challenging to piece this together from all the information, but here's how to do this in NodeJS. Note that the latest Node supports built-in crypto, which makes it much much easier. Here's my code with the necessary comments.
const jwt = require('jsonwebtoken');
const fs = require('fs');
const {X509Certificate} = require('crypto');
async function decode(signedInfo) {
// MARK: - Creating certs using Node's new build-in crypto
function generateCertificate(cert) {
// MARK: - A simple function just like the PHP's chunk_split, used in generating pem.
function chunk_split(body, chunklen, end) {
chunklen = parseInt(chunklen, 10) || 76;
end = end || '\n';
if (chunklen < 1) {return false;}
return body.match(new RegExp(".{0," + chunklen + "}", "g")).join(end);
}
return new X509Certificate(`-----BEGIN CERTIFICATE-----\n${chunk_split(cert,64,'\n')}-----END CERTIFICATE-----`);
}
// MARK: - Removing the begin/end lines and all new lines/returns from pem file for comparison
function getPemContent(path) {
return fs.readFileSync(path)
.toString()
.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.replace(/[\n\r]+/g, '');
}
// MARK: - The signed info are in three parts as specified by Apple
const parts = signedInfo.split('.');
if (parts.length !== 3) {
console.log('The data structure is wrong! Check it! ');
return null;
}
// MARK: - All the information needed for verification is in the header
const header = JSON.parse(Buffer.from(parts[0], "base64").toString());
// MARK: - The chained certificates
const certificates = header.x5c.map(cert => generateCertificate(cert));
const chainLength = certificates.length;
// MARK: - Leaf certificate is the last one
const leafCert = header.x5c[chainLength-1];
// MARK: - Download .cer file at https://www.apple.com/certificateauthority/. Convert to pem file with this command line: openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem
const AppleRootCA = getPemContent('AppleRootCA-G3.pem');
// MARK: - The leaf cert should be the same as the Apple root cert
const isLeafCertValid = AppleRootCA === leafCert;
if (!isLeafCertValid) {
console.log('Leaf cert not valid! ');
return null;
}
// MARK: If there are more than one certificates in the chain, we need to verify them one by one
if (chainLength > 1) {
for (var i=0; i < chainLength - 1; i++) {
const isCertValid = certificates[i].verify(certificates[i+1].publicKey);
if (!isCertValid) {
console.log(`Cert ${i} not valid! `);
return null;
}
}
}
return jwt.decode(signedInfo);
}
Good luck!
You need to validate the header and the payload with the sign like says in the WWDC videos:
https://developer.apple.com/videos/play/wwdc2022/10040/
https://developer.apple.com/videos/play/wwdc2021/10174/
But is more complicate than you think to do this if you don't have the knownledge about JWT because there is no documentation from Apple to do this, they only say to you "use your favorite cryptographic library to verify the data".
So after doing a lot of research, finally I found a solution using PHP 8.1 with Laravel.
First you need to install this library https://github.com/firebase/php-jwt:
composer require firebase/php-jwt
Then you need to implement the following method in order to validate the JWT from the transaction:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
...
public function validateJwt($jwt)
{
$components = explode('.', $jwt);
if (count($components) !== 3) {
throw new \Exception('JWS string must contain 3 dot separated component.');
}
$header = base64_decode($components[0]);
$headerJson = json_decode($header,true);
$this->validateAppleRootCA($headerJson);
$jwsParsed = (array) $this->decodeCertificate($jwt, $headerJson, 0);
for ($i = 1; $i < count($headerJson) - 1; $i++) {
$this->decodeCertificate($jwt, $headerJson, $i);
}
// If the signature and the jws is invalid, it will thrown an exception
// If the signature and the jws is valid, it will create the $decoded object
// You can use the $decoded object as an array if you need:
$transactionId = $jwsParsed['transactionId'];
}
private function validateAppleRootCA($headerJson)
{
$lastIndex = array_key_last($headerJson['x5c']);
$certificate = $this->getCertificate($headerJson, $lastIndex);
// As Oliver Zhang says in their NodeJS example, download the .cer file at https://www.apple.com/certificateauthority/. Convert to pem file with this command line: openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem
// In Laravel, this location is at storage/keys/AppleRootCA-G3.pem
$appleRootCA = file_get_contents(storage_path('keys/AppleRootCA-G3.pem'));
if ($certificate != $appleRootCA) {
throw new \Exception('jws invalid');
}
}
private function getCertificate($headerJson, $certificateIndex)
{
$certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
$certificate .= chunk_split($headerJson['x5c'][$certificateIndex],64,PHP_EOL);
$certificate .= '-----END CERTIFICATE-----'.PHP_EOL;
return $certificate;
}
private function decodeCertificate($jwt, $headerJson, $certificateIndex)
{
$certificate = $this->getCertificate($headerJson, 0);
$cert_object = openssl_x509_read($certificate);
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
$publicKey = $pkey_array['key'];
$jwsParsed = null;
try {
$jwsDecoded = JWT::decode($jwt, new Key($publicKey, 'ES256'));
$jwsParsed = (array) $jwsDecoded;
} catch (SignatureInvalidException $e) {
throw new \Exception('signature invalid');
}
return $jwsParsed;
}
To call the function, you need to pass the jwt from the transaction:
$jwt = 'eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUl...';
validateJwt($jwt);
The JWS x5c header parameter contains the entire certificate chain used to sign and validate the JWS. There is no need to fetch any other certificates or keys.
The RFC specifies that the certificate corresponding to the public key that was used to sign the JWS must be the first certificate.
You can extract the public key from this certificate and use it to verify the JWS signature. There is some guidance on this in this answer
One of the great improvements in StoreKit2 is that you are no longer required to use a server to validate in app purchase transactions securely.
Apple's WWDC 2021 session on StoreKit2 describes the content of the JWS and also shows how to validate on device that the JWS was actually generated for that device.
But, what if you do want to validate the transaction on a server? Since the x5c claim contains the certificate chain, an attacker could sign a forged JWS with their own certificate and include that certificate in the x5c claim.
The answer is that you have your app send the original transaction id to your server along with any other information you need, such as the user's account identifier. Your server can then request the corresponding JWS from Apple and validate the signature of the returned JWS.
As the JWS was fetched from Apple by your server code it can be sure that it is not a spoofed JWS.
If possible, include an appAccountToken in your purchase request and either determine the expected token value based on the user's authentication to your server or (less effective) have your app supply the token when it supplies the original transaction id. You can then verify the token value in the JWS matches the expected value. This makes it harder for an attacker to replay some other purchase event.

Simulate an HTTP/2 stream with pcap4j

I'm trying to capture the unencrypted bytes of a TLS connection and record them into a cap file for analysis of the HTTP/2 traffic. There are a lot of assumptions I am making that this is even possible. But I'm willing to fudge almost everything below the HTTP/2 layer if I can see that traffic in a useful tool like Wireshark.
https://github.com/yschimke/okhttp/commit/c6b0b4c0ba3b59d44cf292955eef2685ed6094e7#diff-d4b38ff70d61641e49af93db2892080f47a2480af92ca151b2daabb50bbc459b
My approach eventually boils down to
return object : DelegatingSSLSocket(socket) {
override fun getInputStream(): InputStream {
return object : FilterInputStream(socket.inputStream) {
override fun read(b: ByteArray, off: Int, len: Int): Int {
return super.read(b, off, len).also { readLen ->
dumper.dump(
ipv4ReadPacketBuilder.payloadBuilder(
tcpReadPacketBuilder
.payloadBuilder(
UnknownPacket.Builder().rawData(
b.sliceArray(off.rangeTo(off + readLen))
)
)
)
.build()
)
}
}
}
}
override fun getOutputStream(): OutputStream {
return object : FilterOutputStream(socket.outputStream) {
override fun write(b: ByteArray, off: Int, len: Int) {
super.write(b, off, len)
dumper.dump(
ipv4WritePacketBuilder.payloadBuilder(
tcpWritePacketBuilder
.payloadBuilder(
UnknownPacket.Builder().rawData(b.sliceArray(off.rangeTo(off + len)))
)
)
.build()
)
}
}
}
}
Does anyone have any advice on pcap4j or pcap files generally to see what I'm doing wrong?
The packets I'm writing are IPv4>TCP>Data
But Wireshark shows
For IPv4, Version is always equal to 4. Your image states that you are trying to write IPv4 Header but hex codes shows that it is not the IPv4 header.
First highlighted number is 56. Instead of 5 it should be 4. Hence Wireshark is unable to detect it as a IPv4 packet.
Refer my below link, it will help you to understand the sample format.
How to obtain the source IP from a Wireshark dump of an HTTP GET request
For TCP, it should be 06 instead of bb.
Also your source IP is 0.0.0.0. It will not generate any error but you can change it as per your requirement.

Solve issue POSTING to webhook for IFTTT from Arduino MKR1010

I am aiming to make a post request to trigger a IFTTT webhook action. I am using the MKR1010 board. I am able to connect to the network and turn the connected LED on and off using the cloud integration.
The code is as follows, but doesn't trigger the web hook. I can manually paste the web address in a browser and this does trigger the web hook. When the code is posted it returns a 400 bad request error.
The key has been replaced in the below code with a dummy value.
Does anybody know why this is not triggering the web hook? / Can you explain why the post request is being rejected by the server? I don't even really need to read the response from the server as long as it is sent.
Thank you
// ArduinoHttpClient - Version: Latest
#include <ArduinoHttpClient.h>
#include "thingProperties.h"
#define LED_PIN 13
#define BTN1 6
char serverAddress[] = "maker.ifttt.com"; // server address
int port = 443;
WiFiClient wifi;
HttpClient client = HttpClient(wifi, serverAddress, port);
// variables will change:
int btnState = 0; // variable for reading the pushbutton status
int btnPrevState = 0;
void setup() {
// Initialize serial and wait for port to open:
Serial.begin(9600);
// This delay gives the chance to wait for a Serial Monitor without blocking if none is found
delay(1500);
// Defined in thingProperties.h
initProperties();
// Connect to Arduino IoT Cloud
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
/*
The following function allows you to obtain more information
related to the state of network and IoT Cloud connection and errors
the higher number the more granular information you’ll get.
The default is 0 (only errors).
Maximum is 4
*/
setDebugMessageLevel(2);
ArduinoCloud.printDebugInfo();
// setup the board devices
pinMode(LED_PIN, OUTPUT);
pinMode(BTN1, INPUT);
}
void loop() {
ArduinoCloud.update();
// Your code here
// read the state of the pushbutton value:
btnState = digitalRead(BTN1);
if (btnPrevState == 0 && btnState == 1) {
led2 = !led2;
postrequest();
}
digitalWrite(LED_PIN, led2);
btnPrevState = btnState;
}
void onLed1Change() {
// Do something
digitalWrite(LED_PIN, led1);
//Serial.print("The light is ");
if (led1) {
Serial.println("The light is ON");
} else {
// Serial.println("OFF");
}
}
void onLed2Change() {
// Do something
digitalWrite(LED_PIN, led2);
}
void postrequest() {
// String("POST /trigger/btn1press/with/key/mykeyhere")
Serial.println("making POST request");
String contentType = "/trigger/btn1press/with/key";
String postData = "mykeyhere";
client.post("/", contentType, postData);
// read the status code and body of the response
int statusCode = client.responseStatusCode();
String response = client.responseBody();
Serial.print("Status code: ");
Serial.println(statusCode);
Serial.print("Response: ");
Serial.println(response);
Serial.println("Wait five seconds");
delay(5000);
}
Why do you want to make a POST request and send the key in the POST body? The browser sends a GET request. It would be
client.get("/trigger/btn1press/with/key/mykeyhere");
In HttpClient post() the first parameter is 'path', the second parameter is contentType (for example "text/plain") and the third parameter is the body of the HTTP POST request.
So your post should look like
client.post("/trigger/btn1press/with/key/mykeyhere", contentType, postData);

X509Certificate2 Public Key Signature Algorithm http://www.w3.org/2001/04/xmldsig-more#rsa-sha256

I have a certificate that has a Signature algorithm of sha256RSA but when the cert is loaded into an X509Certificate2 object the Public Key Signature algorithm is http://www.w3.org/2000/09/xmldsig#rsa-sha1 but I would expect it to be http://www.w3.org/2001/04/xmldsig-more#rsa-sha256. Below is a quick sample of how I am grabbing the cert and viewing the signature algorithm. The reason this is an issue is that Web Service Enhancements (WSE) uses this value to map to CryptoConfig to find the SignatureFormatter. Without loading the rsa-sha256 formatter we are not able to support SHA256 signatures. Moving away from WSE to something like WCF is not really an option for us at this point. I have wondered if it might have something to do with not using the Enhanced CSP but don't know how to force that either. Any help/thoughts are much appreciated.
private void button1_Click(object sender, EventArgs e)
{
textBox1.Text = String.Empty;
List<X509Certificate2> certs = LoadX509Certificates();
foreach (X509Certificate2 cert in certs)
{
textBox1.Text += cert.PublicKey.Key.SignatureAlgorithm + "\r\n";
}
}
public List<X509Certificate2> LoadX509Certificates()
{
List<X509Certificate2> certificateList = null;
// copied implementation from X509TokenProvider but don't throw on more than on certificate
X509Certificate2Collection certificateCollection = null;
X509Store x509Store = new X509Store("My",
StoreLocation.LocalMachine);
try
{
x509Store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
certificateCollection = x509Store.Certificates.Find(X509FindType.FindByThumbprint,
"93 f4 3a cd b2 6c 79 74 1c 55 4f d1 43 94 37 30 98 82 48 74", false);
if (certificateCollection.Count < 1)
{
throw new ArgumentException(
string.Format("No certificate found"));
}
foreach (X509Certificate2 cert in certificateCollection)
{
if (cert.HasPrivateKey)
{
var tempKey = cert.PrivateKey;
}
}
certificateList = certificateCollection.Cast<X509Certificate2>().ToList();
}
finally
{
if (certificateCollection != null)
{
foreach (X509Certificate2 x509Certificate2 in x509Store.Certificates)
{
if (!certificateCollection.Contains(x509Certificate2))
{
x509Certificate2.Reset();
}
}
}
x509Store.Close();
}
return (certificateList);
}
Certificate details from OpenSSL
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
2a:ac:6c:cc:22:6e:cb:97:4f:2a:4a:91:42:00:74:b4
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=\x00_\x00R\x00o\x00o\x00t\x00 \x00C\x00A\x00 \x00T\x00e\x00s\x00t\x00_\x002\x005\x006\x00.\x00e\x00t\x00.\x00l\x00o\x00c\x00a\x00l
Validity
Not Before: Feb 9 14:21:51 2015 GMT
Not After : Dec 31 23:59:59 2039 GMT
Subject: CN=\x00C\x00l\x00i\x00e\x00n\x00t\x00_\x00T\x00e\x00s\x00t\x00_\x002\x005\x006\x00.\x00e\x00t\x00.\x00l\x00o\x00c\x00a\x00l
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public Key: (1024 bit)
Modulus (1024 bit):
00:af:77:1c:64:3a:ea:0b:72:df:e7:6d:c0:f6:74:
df:21:9c:e4:98:07:4c:b5:d9:7d:a3:96:88:a8:eb:
fd:bf:d6:8c:71:ac:3d:38:c2:42:b4:1d:83:18:d7:
2b:80:a2:06:3d:74:99:64:fe:a8:47:52:0e:d1:a2:
ff:8a:5d:af:a3:a9:4e:27:3e:2c:30:48:68:22:76:
ea:9a:e3:0f:d5:fa:e9:5c:35:f9:d2:dd:28:55:40:
ec:52:86:b9:c0:f9:30:c6:2d:94:0a:3b:7a:0f:00:
25:c9:eb:04:6c:85:d6:3e:6b:14:7e:a4:aa:8e:1b:
90:72:c0:76:91:f6:7b:e6:15
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
2.5.29.1:
0g....b....O"....o.q.A0?1=0;..U...4._.R.o.o.t. .C.A. .T.e.s.t._.2.5.6...e.t...l.o.c.a.l......B.L.#......p
Signature Algorithm: sha256WithRSAEncryption
22:16:ad:44:69:27:67:93:0d:e7:43:4a:53:ee:58:ec:b1:56:
08:b2:49:fe:0d:3d:53:83:71:01:12:a7:0b:f5:d6:47:1c:5d:
f2:00:9b:61:0f:17:13:aa:24:0e:f4:db:97:85:da:47:e8:4c:
39:7a:52:ee:4b:ac:8c:f5:25:33:9f:aa:33:53:c5:8d:b3:c6:
27:e4:92:b3:b8:d2:aa:a9:b4:f0:8a:83:89:34:35:65:b2:69:
d0:4c:c1:48:f0:ea:01:a2:aa:80:d6:fb:f6:09:02:ff:00:10:
19:94:ad:20:f0:92:27:6b:6c:75:72:c4:04:a1:40:4b:16:60:
84:fe
You are confused by property name.
sha256RSA is certificate signature algorithm and is not related to embedded public key at all. SignatureAlgorithm is a part of X509Certificate2 object, not public key. What you see in the RSACryptoServiceProvider.SignatureAlgorithm property is a default signature algorithm in a particular implementation of an RSA algorithm. And as per documentation, it is always set to http://www.w3.org/2000/09/xmldsig#rsa-sha1

Resources