3

Update 2:

Got the complete certificate chain from Swish support and the production servers root cert. It still does not work. Works fine locally and on VM with all updates as well.

Tested with ServicePointManager.Expect100Continue = false;

Setting the following values in Web.config:

<system.web>
    <compilation debug="true" targetFramework="4.8"/>
    <httpRuntime targetFramework="4.8" enableVersionHeader="false" maxRequestLength="102400" executionTimeout="3600"/>

Loading certificates:

    var certThumbprint = "1234"; //1234567890 Swish number cert
    var root1Thumbprint = "1234"; //Handelsbanken Customer CA1 v2 for Swish
    var root2Thumbprint = "1234"; //Handelsbanken Root CA v2 for Swish
    var root3Thumbprint = "49f2334991c4a48dfc6862095e965229b7cee457"; //Swish Root CA v2
    var root4Thumbprint = "A8985D3A65E5E5C4B2D7D66D40C6DD2FB19C5436"; //DigiCert Global Root CA
    var thumbprints = new[] { certThumbprint, root1Thumbprint, root2Thumbprint, root3Thumbprint, root4Thumbprint };

Exception is now:

{
    "Message": "An error has occurred.",
    "ExceptionMessage": "One or more errors occurred.",
    "ExceptionType": "System.AggregateException",
    "StackTrace": "   at Project.Web.Services.SwishService.SendPaymentRequest(PaymentRequest paymentRequest) in C:\\Users\\oscar\\source\\repos\\Project\\src\\Project.Web\\Services\\SwishService.cs:line 131\r\n   at Project.Web.Services.SwishService.PopulateAndSendPaymentRequest(String userMobileNumber, Contract contract) in C:\\Users\\oscar\\source\\repos\\Project\\src\\Project.Web\\Services\\SwishService.cs:line 159\r\n   at Project.Web.Controllers.Employees.EmployeeController.<SignAndPayContract>d__16.MoveNext() in C:\\Users\\oscar\\source\\repos\\Project\\src\\Project.Web\\Controllers\\Employees\\EmployeeController.cs:line 256",
    "InnerException": {
        "Message": "An error has occurred.",
        "ExceptionMessage": "An error occurred while sending the request.",
        "ExceptionType": "System.Net.Http.HttpRequestException",
        "StackTrace": null,
        "InnerException": {
            "Message": "An error has occurred.",
            "ExceptionMessage": "The underlying connection was closed: An unexpected error occurred on a send.",
            "ExceptionType": "System.Net.WebException",
            "StackTrace": "   at System.Net.HttpWebRequest.EndGetRequestStream(IAsyncResult asyncResult, TransportContext& context)\r\n   at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)",
            "InnerException": {
                "Message": "An error has occurred.",
                "ExceptionMessage": "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.",
                "ExceptionType": "System.IO.IOException",
                "StackTrace": "   at System.Net.TlsStream.EndWrite(IAsyncResult asyncResult)\r\n   at System.Net.PooledStream.EndWrite(IAsyncResult asyncResult)\r\n   at System.Net.ConnectStream.WriteHeadersCallback(IAsyncResult ar)",
                "InnerException": {
                    "Message": "An error has occurred.",
                    "ExceptionMessage": "An existing connection was forcibly closed by the remote host",
                    "ExceptionType": "System.Net.Sockets.SocketException",
                    "StackTrace": "   at System.Net.Sockets.Socket.BeginReceive(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags, AsyncCallback callback, Object state)\r\n   at System.Net.Sockets.NetworkStream.BeginRead(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)"
                }
            }
        }
    }
}

Update:

More than six years later I had to do this again and it was still not straight forward. However now it should finally work in an Azure Web App and it seems to have been working for a while looking at @AndersEkdahl answer.

The developer environment has a lot of good information and certificates:

https://developer.swish.nu/documentation/environments

Production certificates can be downloaded here:

https://portal.swish.nu/company/login?redirectPath=/company/certificates

When I had completed the Swish Certificate Request tutorial on https://portal.swish.nu and downloaded my .pem file I completed it via IIS Manager and "Complete Certificate Request". Of course on the same computer that created the certificate request.

My downloaded .pem file was called:

swish_certificate_202306201123.pem

enter image description here

After doing this I could export my certificate to .pfx.

However this did not work neither locally or in my Azure Web App. If I tried to use the certificate I got the same error as in this question:

Could not create SSL/TLS secure channel

Looking at the certification path this became quite clear since the issuer could not be found.

enter image description here

Go back to the swish_certificate_202306201123.pem file and open this is notepad or similar text editor. You should see three -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----. Create one file for each of these certificates. I named them 1.cer, 2.cer and 3.cer. 1.cer is the certificate you have already imported. However 2.cer and 3.cer needs to be imported to Trusted Root Certification Authorities.

enter image description here

After doing this the certificate chain is now trusted.

enter image description here

Now I could follow Microsoft guide for Use a TLS/SSL certificate in your code in Azure App Service - Load certificate in Windows apps

https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate-in-code#load-certificate-in-windows-apps

The code for loading a certificate is from Microsoft:

Example:

string certThumbprint = "E661583E8FABEF4C0BEF694CBC41C28FB81CD870";
bool validOnly = false;

using (X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
  certStore.Open(OpenFlags.ReadOnly);

  X509Certificate2Collection certCollection = certStore.Certificates.Find(
                              X509FindType.FindByThumbprint,
                              // Replace below with your certificate's thumbprint
                              certThumbprint,
                              validOnly);
  // Get the first cert with the thumbprint
  X509Certificate2 cert = certCollection.OfType<X509Certificate2>().FirstOrDefault();

  if (cert is null)
      throw new Exception($"Certificate with thumbprint {certThumbprint} was not found");

  // Use certificate
  Console.WriteLine(cert.FriendlyName);
  
  // Consider to call Dispose() on the certificate after it's being used, avaliable in .NET 4.6 and later
}

Now Swish for production worked locally.

However after uploading the .pfx file and the two .cer files from Trusted Root Certification Authorities and including them all via WEBSITE_LOAD_CERTIFICATES it still did not work.

enter image description here

enter image description here

I verified that the app could load my certificates via Development Tools -> Advanced Tools -> Debug console:

dir cert:/CurrentUser/My

enter image description here

Code for loading certificates looks like this:

var thumbprints = new[] { certThumbprint, root1Thumbprint, root2Thumbprint };

var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);

var certificates = new X509Certificate2Collection();

foreach (var thumbprint in thumbprints)
{
    var certs = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
    certificates.AddRange(certs);
}

var handler = new HttpClientHandler()
{
    ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
    {
        return true;
    }
};

handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
foreach (var cert in certificates)
{
    handler.ClientCertificates.Add(cert);
}

client = new HttpClient(handler);

When running this code locally I had to copy the two new certificates in Trusted Root Certification Authorities to Personal since the certificate chain is fetched from there.

I thought handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12; would be the error but I suspect I need to load Swish Root CA v2.

Source for troubleshooting:

https://johan.driessen.se/posts/Calling-the-Swish-Payment-API-from-Azure-AppService/

I have contacted Swish support about the missing certificate chain since they state on their web page:

The complete certificate chain of the Swish server TLS certificate is available through Swish Certificate Management.

I can not find it there though.

If you decide to host via VM/IIS remember to use StoreLocation.LocalMachine and grant access to the certificate for the user running the application pool.

https://serverfault.com/a/132791/293367

I solved it like this to use StoreLocation.CurrentUser locally:

var swishCertificateStoreLocation = ConfigurationManager.AppSettings["SwishCertificateStoreLocation"];

Enum.TryParse(swishCertificateStoreLocation, out StoreLocation storeLocation);

var certStore = new X509Store(StoreName.My, storeLocation);

Original:

Before writing this question I have gone through a lot of questions and answers but I can't seem to find a solution. What I'm trying to do is host an application as a Azure App Service that needs to make a call to the Swish API.

Please see this thread for how my implementation runs locally which works fine:

C# HttpClient with X509Certificate2 - WebException: The request was aborted: Could not create SSL/TLS secure channel

System diagnostics log from Azure:

https://pastebin.com/EBFb3zrA

I have tried the solutions from Microsoft forums and SO but none seem to do the trick:

https://social.msdn.microsoft.com/Forums/azure/en-US/ca6372be-3169-4fb5-870f-bfbea605faf6/azure-webapp-webjob-exception-could-not-create-ssltls-secure-channel?forum=windowsazurewebsitespreview

//Tested both, none of them work
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11

Since a lot of the questions are based on accessing an external service and not sending a client certificate the complexity rises a bit as well.

What I have done is in the SSL certificates tab on Azure import the Test certificate. Since .p12 and .pfx are both PKCS #12 files I just renamed the .p12-file. The application runs as B1 Basic App Service Plan so most functionality should be present.

https://stackoverflow.com/a/6821061/3850405

I have also tried this guide to add the certificate to the certificate store in Azure -> Application settings -> App Settings:

https://azure.microsoft.com/en-us/blog/using-certificates-in-azure-websites-applications/

When this did not work I tried to add WEBSITE_LOAD_CERTIFICATES to appSettings in my application but it resulted in a HTTP 503.

Swish certificate and English guide:

https://www.getswish.se/content/uploads/2015/06/Guide-Testverktyg_20151210.zip

Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • 1
    As I known, The certificate you uploaded to your web app will only be installed to the Personal certificate store. While I followed your swish certificate guide, I found it would be stored into `Trusted Root Certification Authorities`, I assumed that it could be the cause. – Bruce Chen May 30 '17 at 09:46
  • @Bruce-MSFT Yes I think this is the problem as well. Running it as a `virtual machine` now and it works fine. Would be nice if it were possible to do it as `App Service` though. – Ogglas May 30 '17 at 14:59
  • Did you manage to get this to work? Im looking for the same thing here. – JeppePepp Sep 19 '18 at 19:07
  • @JeppePepp No it will not work. I asked Microsoft directly about trusting their certificate but the request was denied. We solved it by hosting it as a Virtual machine and from there add their certificate to Trusted Root Certification Authorities. https://feedback.azure.com/forums/169385-web-apps/suggestions/19533202-ability-to-use-the-swedish-payment-solution-swish – Ogglas Sep 19 '18 at 19:51
  • Alright thanks for your answer. I know this question is quite old so I was crossing my fingers u had some great news to share. I'll stick with klarna for now :) – JeppePepp Sep 19 '18 at 20:03

3 Answers3

3

We were able to make this work in an Azure Web App. The trick was the appSetting WEBSITE_LOAD_CERTIFICATES and to upload all Swish certificates in the Azure portal. That is, the .pfx file from Swish contains three certs where two of them are root certs. So we exported the root certs and uploaded the .cer files under TLS/SSL settings -> Public Key Certificates (.cer) and then it started to work. You also need to upload them to all deployment slots since the certs will not be automatically copied to the slots.

Anders Ekdahl
  • 22,685
  • 4
  • 70
  • 59
  • Did you have to something else in the code to use the root certs? When I create my HttpClient, I add the client certificate that I find with `certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false)`, but that only seems to add the client cert itself, and no the root certs. Do you add them to `HttpClientHandler.ClientCertificates` as well? – Johan Driessen Mar 12 '20 at 11:11
  • Yes, exactly, we add them to `HttpClientHandler.ClientCertificates`. – Anders Ekdahl Mar 12 '20 at 12:40
  • 1
    The answer here is absolutely correct. However, if anyone would like a more comprehensive guide how to get this working, I've written one on my blog: https://johan.driessen.se/posts/Calling-the-Swish-Payment-API-from-Azure-AppService/ – Johan Driessen Mar 16 '20 at 09:50
  • Great that this works now! I tested `WEBSITE_LOAD_CERTIFICATES` back in 2017 but then it did not work. – Ogglas Jan 24 '23 at 10:16
0

According to your scenario, I also checked by adding cert file to a Resources File and construct the X509Certificate2 instance by the following code snippet:

var certBytes=(byte[])ResourceManager.GetObject("Swish_Certificate");
var certificate = new X509Certificate2(certBytes, "swish");

Per my test, I assumed that you could not implement Swish payment on Azure Web App. Additionally, you could add your feedback for azure web app here.

Bruce Chen
  • 18,207
  • 2
  • 21
  • 35
0

Update:

Reply from Swish:

In our logs we can see that the certificate count is (1) instead of (3). This means that there is nothing wrong with the certificates because when they are used as intended and all are included in the call, the SSL handshake is processed correctly and a session is established.

...

However, we know from experience that using client certificates on Azure servers can be a bit unstable, as we have seen cases like this before where integrations have stopped working without any manual changes being made from either the client side or the Swish side and even where integrations just starts working again without any action taken. Some merchant set up brand new in Azure and got it working again after this.

I now have two domains up and running and they have been working for over a month. Without doing any changes to the App Service or deploying new code it simply started working after a day or two. It did not work at first even if I restarted the application manually or stopped it and then started it. Given the reply from Swish it does not sound like certificates are available after restart even if documentation says that they should be. Sounds like a bug in App Service with certificates.

If WEBSITE_LOAD_CERTIFICATES is set to *, restart the app to make the new certificate accessible.

https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate-in-code#when-updating-renewing-a-certificate

Original:

This was a pain to get it working as an Azure Web App so if it helps anyone I hope this complete answer will help.

I needed to contact Swish support to get Swish Root CA v2 certificate.

Add WEBSITE_LOAD_CERTIFICATES * to Azure Web App Configuration Application settings.

When setting up a new domain it did not work right away either. I had to wait until the application idled out (Always on set to Off). It did not work to stop the App Service or force it to restart. I have contacted Swish why it started working without any changes either in code or server configuration. It could be a block of some Azure Outbound IP addresses by Swish or that certificates for some reason can not be picked up right away.

enter image description here

https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate-in-code#make-the-certificate-accessible

Upload your .pfx file and the four .cer files for the complete certificate chain under Azure Web App Certificates.

In my case it looks like this:

enter image description here

Code for SwishService.cs:

public class SwishService
{
    private HttpClient client;
    private string baseUrl = "";
    private string paymentPath = "/swish-cpcapi/api/v1/paymentrequests/";
    private string certificateThumbprint = "";
    //refunds
    //https://mss.swicpc.bankgirot.se/swish-cpcapi/api/v1/refunds/

    public SwishService()
    {
        //Set TLS 1.2 as default connection
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

        ServicePointManager.Expect100Continue = false;

        baseUrl = ConfigurationManager.AppSettings["SwishBaseUrl"]; //https://cpc.getswish.net/
        certificateThumbprint = ConfigurationManager.AppSettings["SwishCertificateThumbprint"];

        var baseAddress = new Uri(baseUrl);

        var thumbprints = new[] { certificateThumbprint };

        var swishCertificateStoreLocation = ConfigurationManager.AppSettings["SwishCertificateStoreLocation"]; //CurrentUser

        Enum.TryParse(swishCertificateStoreLocation, out StoreLocation storeLocation);

        var certStore = new X509Store(StoreName.My, storeLocation);
        certStore.Open(OpenFlags.ReadOnly);

        var certificates = new X509Certificate2Collection();

        foreach (var thumbprint in thumbprints)
        {
            var certs = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
            certificates.AddRange(certs);
        }

        var handler = new HttpClientHandler()
        {
            ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
            {
                if (sslPolicyErrors == SslPolicyErrors.None)
                {
                    return true;   //Is valid
                }
                //Will be hosted as Azure Web application and we therefore need to trust Swish certificates
                var certificateThumbprint = ConfigurationManager.AppSettings["SwishCertificateThumbprint"]; //1234567890  for prod
                var certThumbprint = "1234"; //1234567890 
                var root1Thumbprint = "1234"; //Handelsbanken Customer CA1 v2 for Swish
                var root2Thumbprint = "1234"; //Handelsbanken Root CA v2 for Swish
                var root3Thumbprint = "49f2334991c4a48dfc6862095e965229b7cee457"; //Swish Root CA v2
                var root4Thumbprint = "A8985D3A65E5E5C4B2D7D66D40C6DD2FB19C5436"; //DigiCert Global Root CA
                var trustedThumbprints = new[] { certificateThumbprint, certThumbprint, root1Thumbprint, root2Thumbprint, root3Thumbprint, root4Thumbprint };

                if (trustedThumbprints.Any(x => x == certificate.GetCertHashString()))
                {
                    return true;
                }

                return false;
            }
        };

        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
        foreach (var cert in certificates)
        {
            handler.ClientCertificates.Add(cert);
        }

        client = new HttpClient(new LoggingHandler(handler));
        client.BaseAddress = baseAddress;
        client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
    }

    /// <summary>
    /// Poulate PaymentRequest and sends payment request to Swish
    /// </summary>
    /// <param name="userMobileNumber"></param>
    /// <param name="contract"></param>
    /// <returns>Returns ID of payment created</returns>
    public string PopulateAndSendPaymentRequest(string userMobileNumber, Contract contract)
    {
        if (!isValidMobileNumber(ref userMobileNumber)) { throw new Exception($"Phonenumber {userMobileNumber} is not valid!"); }

        var pr = new PaymentRequest
        {
            PayeePaymentReference = $"{contract.Id}",
            // CallbackUrl = "https://requestb.in/", //Use https://requestb.in for local testing
            CallbackUrl = ConfigurationManager.AppSettings["SwishCallbackUrl"], //https://mysite.azurewebsites.net/api/swish/callback
            PayerAlias = userMobileNumber,
            PayeeAlias = ConfigurationManager.AppSettings["SwishPayeeAlias"], //1234567890 
            Amount = CalculateSumToPay(contract),
            Currency = "SEK",
            Message = ConfigurationManager.AppSettings["SwishMessage"]
        };

        var id = SendPaymentRequest(pr);
        return id;
    }

    /// <summary>
    /// Send payment request to Swish
    /// </summary>
    /// <param name="paymentRequest"></param>
    /// <returns>Returns ID of payment created</returns>
    private string SendPaymentRequest(PaymentRequest paymentRequest)
    {
        var content = new StringContent(JsonConvert.SerializeObject(paymentRequest), Encoding.UTF8, "application/json");

        try
        {
            var response = client.PostAsync(paymentPath, content).Result;

            if (response.StatusCode == HttpStatusCode.Created)
            {
                if (response.Headers.Location != null)
                {
                    var path = response.Headers.Location.AbsolutePath;
                    var id = path.Replace(paymentPath, "");
                    return id;
                }
            }

            throw new Exception($"Payment request was not sent. Response body:{response.Content.ReadAsStringAsync().Result} Response:{response}");
        }
        catch (Exception e)
        {
            throw e;
        }
    }

LoggingHandler.cs

public class LoggingHandler : DelegatingHandler
{
    public LoggingHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    {
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("Request:");
        Debug.WriteLine(request.ToString());
        if (request.Content != null)
        {
            Debug.WriteLine(request.Content.ReadAsStringAsync().Result);
        }
        Debug.WriteLine("");

        HttpResponseMessage response = base.SendAsync(request, cancellationToken).Result;

        Debug.WriteLine("Response:");
        Debug.WriteLine(response.ToString());
        if (response.Content != null)
        {
            //Debug.WriteLine(response.Content.ReadAsStringAsync().Result);
        }
        Debug.WriteLine("");

        return response;
    }
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418