Safari Push Package: how to fix Apple's expired WWDR certificate - c#

Well, we all knew that, that was about to happen, the Apple WWDR certificate has expired on Valentine's Day (that's what I call "developer love") according to the news release from Apple.
I'm using C# to generate a Push Package for Safari, and, surprise, this does not work any-more. This is the message that I get in my logging endpoint instead:
{"logs":["Signature verification of push package failed"]}
This is how my old PKCS#7 Signing code looked like:
// Sign the message with the private key of the signer.
static byte[] PKCS7SignMessage(byte[] message, X509Certificate2 signerCertificate)
{
// Place message in a ContentInfo object.
// This is required to build a SignedCms object.
ContentInfo contentInfo = new ContentInfo(message);
// Instantiate SignedCms object with the ContentInfo above.
// Has default SubjectIdentifierType IssuerAndSerialNumber.
// Has default Detached property value false, so message is
// included in the encoded SignedCms.
SignedCms signedCms = new SignedCms(contentInfo, true);
// Formulate a CmsSigner object for the signer.
CmsSigner cmsSigner = new CmsSigner(signerCertificate);
cmsSigner.IncludeOption = X509IncludeOption.EndCertOnly;
// Sign the CMS/PKCS #7 message.
signedCms.ComputeSignature(cmsSigner);
// Encode the CMS/PKCS #7 message.
return signedCms.Encode();
}
Apple asks to also "pass the path to the renewed intermediate for the extra certificates parameter".
So I tried this:
X509Certificate2 appleIntermediate = new X509Certificate2();
appleIntermediate.Import(#"Path-to-new-WWRD.cer");
cmsSigner.Certificates.Add(appleIntermediate);
It didn't work. (Signature verification of push package failed)
Later I tried to change this line:
cmsSigner.IncludeOption = X509IncludeOption.WholeChain;
It didn't work. I've got an exception saying:
"A certificate chain could not be built to a trusted root authority".
All right, now I decided to:
Add All of Apple CA root certificates to the local machine's trusted certificate store.
Add the renewed WWRD certificate to the local machine's intermediate certificate store.
Restart the process and try the code again. Good news, it is now signing again including, in theory, the whole certificate chain.
BUT: It didn't work. (Signature verification of push package failed)
According to Apple, fixing this this is piece of cake:
Safari Push Notifications
Update your notification package signing server to include your web push certificate and the renewed intermediate certificate by February 14, 2016. After this date, new users will not be able to sign up for push notifications from your website until your server has been updated. If you were using the openssl_pkcs7_sign function to sign your push package with only your web push certificate, you should pass the path to the renewed intermediate for the extra certificates parameter.
Now, what does that mean in plan English?
And how can I apply that to a C# context?

Apple does not want the whole chain. They only expect your certificate, and their intermediate certificate to be included. So your code should look something like this:
static public byte[] PKCS7SignMessage(byte[] manifest_data, X509Certificate2 signerCertificate) {
X509Certificate2Collection ca_chain;
ContentInfo content;
SignedCms signed_cms;
CmsSigner signer;
signed_cms = new SignedCms();
ca_chain = new X509Certificate2Collection(new X509Certificate2(#"Path-to-new-intermediate-WWRD.cer"));
content = new ContentInfo(manifest_data);
signed_cms = new SignedCms(content, true);
signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, signerCertificate);
signer.IncludeOption = X509IncludeOption.ExcludeRoot;
signer.Certificates.AddRange(ca_chain);
signed_cms.ComputeSignature(signer);
return signed_cms.Encode();
}

Related

Can a .p12 file with CA certificates be used in C# without importing them to certificate store

I got a .p12 certificate file with 3 certificates in it. 2 of them are CA certificates.
If I use curl (7.70 on Win10) I can do:
curl -s -S -i --cert Swish_Merchant_TestCertificate_1234679304.p12:swish --cert-type p12 --tlsv1.2 --header "Content-Type:application/json" https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests --data-binary #jsondata.json
Curl will use the CA certificates in the p12 file when connecting to the server.
On the other hand, if I try to do something similar in .net core (3.1) it fails with the error message "The message received was unexpected or badly formatted."
var handler = new HttpClientHandler();
var certs = new X509Certificate2Collection();
certs.Import(#"Swish_Merchant_TestCertificate_1234679304.p12", "swish", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
foreach (var cert in certs)
{
handler.ClientCertificates.Add(cert);
}
var client = new HttpClient(handler);
var url = "https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests";
var request = new HttpRequestMessage()
{
RequestUri = new Uri(url),
Method = HttpMethod.Post,
};
request.Content = new StringContent(System.IO.File.ReadAllText(#"jsondata.json"), Encoding.UTF8, "application/json");
request.Headers.Add("accept", "*/*");
var response = await client.SendAsync(request);
Using Wireshark I saw that curl sends all three certificates from the p12 file whereas .net core only sends one. See images below.
If I install the CA certificates into "Personal certificate" for "Current User" then .net core also sends all three certificates and it works.
Question: Do I have to install the CA certificates (into the certificate store) when using .net core or is there a way to make it behave just like curl which uses the certificates from the p12 file?
Wireshark curl:
Wireshark .net core:
Short answer: no*.
Wordier intro: SslStream picks one certificate out of the ClientCertificates collection, using data that (was historically, but no longer generally) is sent by the TLS server about appropriate roots (and if none is applicable then it picks the first thing where HasPrivateKey is true). During the selection process each candidate certificate is checked in isolation, and it asks the system to resolve the chain. On Windows, the selected certificate is then sent down to the system libraries for "we're doing TLS now", which (IIRC) is where the limitations come from. (macOS and Linux builds of .NET Core just try to maintain behavioral parity)
Once the certificate is selected, there's one last chain-walk to determine what certificates to include in the handshake, it's done without the context of anything else from the ClientCertificates collection.
If you know that your collection represents one chain, your best answer is to import the CA elements into your user CertificateAuthority store. That store does not impart any trust to the CA certificates, it's really just a cache that's used when building chains.
Also, you don't want PersistKeySet, and probably don't want MachineKeySet: What is the rationale for all the different X509KeyStorageFlags?
var handler = new HttpClientHandler();
using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
var certs = new X509Certificate2Collection();
certs.Import(#"Swish_Merchant_TestCertificate_1234679304.p12", "swish", X509KeyStorageFlags.DefaultKeySet);
foreach (X509Certificate2 cert in certs)
{
if (cert.HasPrivateKey)
{
handler.ClientCertificates.Add(cert);
}
else
{
store.Add(cert);
}
}
}
var client = new HttpClient(handler);
...
* If your system already has the CA chain imported, it'll work. Alternatively, if the CA chain uses the Authority Information Access extension to publish a downloadable copy of the CA cert, the chain engine will find it, and everything will work.

C# - How to create a CSR with a template, forwarding it to an intermediate server which enrolls the cert from PKI and get back a PKCS#7 in response

I have situation where I have to decouple my edge server with the PKI server. The edge server requires a signed certificate to be installed in it. With current proposed approach, I need to create a CSR with a template received from the PKI policies, forward the csr to the intermediate server which gets the CSR signed and get a PKCS#7 in response from the PKI and then it returns back the PKCS#7 cert to the edge where it bundles with its private key generated in the first step and generated PKC#12, which gets stored in the local machine store for further use. Its a C# code and I'm using X509 certs.
1st method ----------
public static Pkcs10CertificationRequest CreateNReturnCSR(string name)
{
var keyGenerator = new RsaKeyPairGenerator();
keyGenerator.Init(
new KeyGenerationParameters(
new SecureRandom(new CryptoApiRandomGenerator()),
2048));
var keyPair = keyGenerator.GenerateKeyPair();
X509Name name2 = new X509Name("CN=" + name);
Pkcs10CertificationRequest csr = new Pkcs10CertificationRequest("SHA256WITHRSA", name2, keyPair.Public, null, keyPair.Private);
//Where to mention template here?
return csr;
}
2nd method------------
UseCSRNReturnCert(csr) -- Not implemented
3rd method -----------
BundleNInstallCert() -- Not implemented

C# RestSharp Client Certificate getting exception "The SSL connection could not be established"

I programming in C# .Net Core 3.1 and .Net Standard library where this code is put.
I am not getting my http requests with RestSharp with a client certificate that are NOT installed on the server/computer i am not going to have access to the server/computer where this is going to be running later on..
My code:
// Create a RestSharp RestClient objhect with the base URL
var client = new RestClient(_baseAPIUrl);
// Create a request object with the path to the payment requests
var request = new RestRequest("swish-cpcapi/api/v1/paymentrequests");
// Create up a client certificate collection and import the certificate to it
X509Certificate2Collection clientCertificates = new X509Certificate2Collection();
clientCertificates.Import(_certDataBytes, _certificatePassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// Add client certificate collection to the RestClient
client.ClientCertificates = clientCertificates;
//X509Certificate2 certificates = new X509Certificate2(_certDataBytes, _certificatePassword);
//client.ClientCertificates = new X509CertificateCollection() { certificates };
// Add payment request data
request.AddJsonBody(requestData);
var response = client.Post(request);
var content = response.Content;
I have done what ever i can find on internet and i have used the service test environment with there own generated certificate and generated my own in there production environment with the same result..
I have tested set TLS 1.1 or TLS 1.2 to test, they specified TLS 1.1 on there own curl examples.
Anyone got any idea?
Update 2020-01-08 - I have got information from the service tech support that they only see me sending one certificate but when i trying to debug and checking the X509Certificate2Collection i finding 3 certificate. But they saying they only see one?
IMHO, if the service you are trying to access is protected via SSL cert with expecting public key/private key, then without the cert, you cant access it. If thats the intent of the service to be protected, i think you may only do to check the service health check or at max, check if the service is accessible (without having the client cert)
But just as HTTPS, then you can try to download the cert from the browser if you can access the service URL from your browser, say, to access their meta or a simple GET or something. In chrome, you may see a lock symbol (security icon) just before to the URL. click that, and you may possibly download the public cert. You can install that on your machine and try that.
In case, if that service has a pub cert and the access doesnt need the client cert, then above option may work. and i hope you are installing the CERT and access that via your code from the account that has access to the cert. Lets say, you install the cert under the LocalSystem, then you may need administrator access from code/solution to access that cert path. Or install the cert under the current_user path.
I would Check if am able to access the service from browser as the first step.
This is, just from what i have understood from your question. again, if you can explain whether the service is a protected service based on public key/private key, you need to have the access/cert to use that service.
UPDATE:
I have tried few options on my end, by creating a demo api and a demo client to use client certs. With what i have understood from your query, i assume below may be some of your options to try out.
// --- using the cert path if you have the path (instead of the byte[])
var myCert = new X509Certificate2("<path to the client cert.cer/.pfx>", "secure-password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
X509CertificateCollection clientCerts = new X509CertificateCollection();
clientCerts.Add(myCert);
OR
var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine); //replace with appropriate values based on your cert configuration
certStore.Open(OpenFlags.ReadOnly);
//option 1 (ideally a client must have the cert thumbprint instead of a password
var cert = certStore.Certificates.Find(X509FindType.FindByThumbprint, "<cert Thumbprint>", false);
//option 2 (explore other options based on X509NameTypes
var cert = certStore.Certificates.OfType<X509Certificate2>()
.FirstOrDefault(cert => cert.GetNameInfo(X509NameType.DnsName, false) == "mycompany.dns.name.given in the cert");
client.ClientCertificates = new X509CertificateCollection(cert);
I recommend generate your own certificate
public static void MakeCert()
{
var ecdsa = ECDsa.Create(); // generate asymmetric key pair
var req = new CertificateRequest("cn=foobar", ecdsa, HashAlgorithmName.SHA256);
var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(10));
// Create PFX (PKCS #12) with private key
File.WriteAllBytes("c:\\temp\\mycert.pfx", cert.Export(X509ContentType.Pfx, "P#55w0rd"));
// Create Base 64 encoded CER (public key only)
File.WriteAllText("c:\\temp\\mycert.cer",
"-----BEGIN CERTIFICATE-----\r\n"
+ Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)
+ "\r\n-----END CERTIFICATE-----");
}
and then add to request
var client = new RestClient(url);
client.ClientCertificates = new X509CertificateCollection();
client.ClientCertificates.Add(new X509Certificate2("c:\\temp\\mycert.cer"));

X509Chain.Build() succeeding for revoked certificate. C#

I create a certificate request like this:
certreq -new req.inf req-Revoked.req
certreq -submit -attrib "SAN:email=ttesting#Test.Domain&upn=1234567890#Test.Domain" -config Win2K8-64\Test-Win2K8-64-CA req-Revoked.req testerCert-Revoked.cer
certreq -accept testerCert-Revoked.cer
CertUtil -f -p testerCert -exportPFX -user My Testing.Tester.T.1234567890 testerCert-Revoked.pfx
CertUtil -delstore -user My Testing.Tester.T.1234567890
Then I revoke it, via:
CertUtil -revoke <SerialNumber_from_above_Cert>
I then execute this code:
X509Certificate2 certificate = GetCertificate("testerCert-Revoked.pfx", "password"); // helper method loads the pfx from a file
X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
if (!chain.Build(certificate))
{
errorBuffer.Append("Could not build X.509 certificate chain:\r\n");
foreach (X509ChainStatus status in chain.ChainStatus)
{
errorBuffer.AppendFormat(" - {0}\r\n", status.StatusInformation);
}
throw new CryptographicException(errorBuffer.ToString());
}
chain.Build() always returns true. But since the certificate is revoked, it should be False! I've double-checked that the certificate is revoked, and that the serial number is listed in the Server Manager under revoked certificates. I've double-checked that the CURL distribution point urls match on the server and the certificate requests. (they're LDAP urls). CertUtil sees the intermediate .cer file as revoked. But the C# code above doesn't.
This all worked before the original CA expired, and IT rebuilt my test machine with a new cert. I've regenerated the certs with the new CA, and every single unit tests works again, except the ones that deal with revocation. Expired certs work as expected, but not revoked certs.
I'm at a loss for what to do next to get this code working again, and would love some help. Thanks!
Turns out the problem was an application called Tumbleweed. It was installed on the server, and was intercepting all revocation requests. Disabling the Tumbleweed service fixed my problem entirely.
Charles.
The chain is simply a list of the certificates, starting with the one you providing, all the way up to the root certificate. It's built out of whatever certificates can be found -- even if they are out of date, invalid, or revoked.
What you're trying to do is validate the chain. Use the chain.ChainStatus for that.
From the MSDN docs:
The X509Chain object has a global error status called ChainStatus that should be used for certificate validation. The rules governing certificate validation are complex, and it is easy to oversimplify the validation logic by ignoring the error status of one or more of the elements involved. The global error status takes into consideration the status of each element in the chain.

Using SSL and SslStream for peer to peer authentication?

I need to provide secure communication between various processes that are using TCP/IP sockets for communication. I want both authentication and encryption. Rather than re-invent the wheel I would really like to use SSL and the SslStream class and self-signed certificates. What I want to do is validate the remote process's certificate against a known copy in my local application. (There doesn't need to be a certificate authority because I intend for the certificates to be copied around manually).
To do this, I want the application to be able to automatically generate a new certifiate the first time it is run. In addition to makecert.exe, it looks like this link shows a way to automatically generate self-signed certificates, so that's a start.
I've looked at the AuthenticateAsServer and AuthenticateAsClient methods of SslStream. You can provide call-backs for verification, so it looks like it's possible. But now that I'm into the details of it, I really don't think it's possible to do this.
Am I going in the right direction? Is there a better alternative? Has anyone done anything like this before (basically peer-to-peer SSL rather than client-server)?
Step 1: Generating a self-signed certificate:
I downloaded the Certificate.cs class posted by Doug Cook
I used this code to generate a .pfx certificate file:
byte[] c = Certificate.CreateSelfSignCertificatePfx(
"CN=yourhostname.com", //host name
DateTime.Parse("2000-01-01"), //not valid before
DateTime.Parse("2010-01-01"), //not valid after
"mypassword"); //password to encrypt key file
using (BinaryWriter binWriter = new BinaryWriter(
File.Open(#"testcert.pfx", FileMode.Create)))
{
binWriter.Write(c);
}
Step 2: Loading the certificate
X509Certificate cert = new X509Certificate2(
#"testcert.pfx",
"mypassword");
Step 3: Putting it together
I based it on this very simple SslStream example
You will get a compile time error about the SslProtocolType enumeration. Just change that from SslProtocolType.Default to SslProtocols.Default
There were 3 warnings about deprecated functions. I replaced them all with the suggested replacements.
I replaced this line in the Server Program.cs file with the line from Step 2:
X509Certificate cert = getServerCert();
In the Client Program.cs file, make sure you set serverName = yourhostname.com (and that it matches the name in the certificate)
In the Client Program.cs, the CertificateValidationCallback function fails because sslPolicyErrors contains a RemoteCertificateChainErrors. If you dig a little deeper, this is because the issuing authority that signed the certificate is not a trusted root.
I don`t want to get into having the user import certificates into the root store, etc., so I made a special case for this, and I check that certificate.GetPublicKeyString() is equal to the public key that I have on file for that server. If it matches, I return True from that function. That seems to work.
Step 4: Client Authentication
Here's how my client authenticates (it's a little different than the server):
TcpClient client = new TcpClient();
client.Connect(hostName, port);
SslStream sslStream = new SslStream(client.GetStream(), false,
new RemoteCertificateValidationCallback(CertificateValidationCallback),
new LocalCertificateSelectionCallback(CertificateSelectionCallback));
bool authenticationPassed = true;
try
{
string serverName = System.Environment.MachineName;
X509Certificate cert = GetServerCert(SERVER_CERT_FILENAME, SERVER_CERT_PASSWORD);
X509CertificateCollection certs = new X509CertificateCollection();
certs.Add(cert);
sslStream.AuthenticateAsClient(
serverName,
certs,
SslProtocols.Default,
false); // check cert revokation
}
catch (AuthenticationException)
{
authenticationPassed = false;
}
if (authenticationPassed)
{
//do stuff
}
The CertificateValidationCallback is the same as in the server case, but note how AuthenticateAsClient takes a collection of certificates, not just one certificate. So, you have to add a LocalCertificateSelectionCallback, like this (in this case, I only have one client cert so I just return the first one in the collection):
static X509Certificate CertificateSelectionCallback(object sender,
string targetHost,
X509CertificateCollection localCertificates,
X509Certificate remoteCertificate,
string[] acceptableIssuers)
{
return localCertificates[0];
}
you can look too this example
Sample Asynchronous SslStream Client/Server Implementation
http://blogs.msdn.com/joncole/archive/2007/06/13/sample-asynchronous-sslstream-client-server-implementation.aspx
if certificate is not produced correctly you can get exception The server mode SSL must use a certificate with the associated private key.
basic certificate example
makecert -sr LocalMachine -ss My -n CN=Test -sky exchange -sk 123456
or
as external file
makecert -sr LocalMachine -ss My -n CN=Test -sky exchange -sk 123456 c:\Test.cer
Certificate Creation Tool (Makecert.exe)
http://msdn.microsoft.com/en-us/library/bfsktky3%28VS.80%29.aspx
What you're proposing sounds fine to me, except that it sounds like you're looking to wait until the callback is invoked in order to generate the certificate. I don't think that that will fly; AFAIK, you've got to provide a valid certificate when you invoke AuthenticateAsX.
However, these classes are overridable; so in theory, you could create a derived class which first checks to see if a certificate needs to be generated, generates it if need be, then invokes the parent AuthenticateAsX method.

Categories

Resources