dotnet / wcf

This repo contains the client-oriented WCF libraries that enable applications built on .NET Core to communicate with WCF services.
MIT License
1.69k stars 556 forks source link

Getting WSDL descriptions securely #5572

Open TehWardy opened 3 weeks ago

TehWardy commented 3 weeks ago

As I said in a comment on #3318 ...

That solves the problem once the client is generated ... How did you solve the issue of getting the WSDL description in the first place with a client cert?

I can't find an answer to this anywhere.

Since this ticket is long closed I figured I ask here instead:

I've been given an SSL cer file and told "here's the URL to our service, add ?wsdl to the end to get the metadata endpoint", but I can't figure out how in the VS UI to get the WSDL description, so I wrote some code to do it and i'm getting a TLS handshake error from the service.

I've raised the question about the error from the server with the service provider but I was hoping there was a built in way to get this.

How do I get the WSDL for a secure service that can only be communicated with using client cert?

TehWardy commented 3 weeks ago

I tried to do this manually using some raw C# but I got a TLS failure from the server ...

using System.Reflection;
using System.Security.Cryptography.X509Certificates;

public class WSDL
{
    public async ValueTask<string> GetDescription(string wsdlUrl, string certResource)
    {
        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(GetCert(certResource));

        handler.ServerCertificateCustomValidationCallback =
            (message, cert, chain, sslPolicyErrors) => true;

        using var client = new HttpClient(handler);
        return await client.GetStringAsync(wsdlUrl);
    }

    X509Certificate2 GetCert(string certResource)
    {
        Stream certStream = Assembly
            .GetExecutingAssembly()
            .GetManifestResourceStream(certResource);

        certStream.Seek(0, SeekOrigin.Begin);

        var memStream = new MemoryStream();
        certStream.CopyTo(memStream);
        return new(memStream.ToArray());
    }
}

Ultimately my question remains the same ... What am I missing here, and is there a way to achieve this in VS UI somewhere?

mconnew commented 3 weeks ago

Did the certificate resource contain the private key as well as the public key? At least on Windows, when you export a certificate including the private key, you are asked to provide a password so I would expect the X509Certificate2 constructor overload to be the one which accepts a password. This suggests to me that you might only have the public certificate in your resource file. Use the extension method GetRSAPrivateKey and check if the private key value is null. If it's null, then you don't have the private key, or it could be a different type of key than RSA. There are equivalent extension methods for different key types. These are DSACertificateExtensions.GetDSAPrivateKey and ECDsaCertificateExtensions.GetECDsaPrivateKey. If you know what type it is, use the relevant method, otherwise you might need to check all three. Basically, this is how you can validate you have the private key in your certificate blob. If you don't have the private key, HttpClientHandler can't use it.

Another useful piece of information to know, if you create an X509Certificate2 instance from a byte array which includes a private key, and you are running on Windows, it creates an ephemeral certificate which includes storing the private key on disk (properly protected so random users can't read it). If you don't Dispose the X509Certificate2 instance before the program exits, you leave this file on disk, and because of the nature of how Windows stores them, there's no way to know that it's not needed any more and you end up using that disk space forever.

If you are hitting a WCF SOAP service, use ?SingleWsdl and not ?Wsdl in the url to get a single file with everything you need in it. If you use ?Wsdl, it contains imports to other files hosted by WCF and you would need to parse the XML, read these url's and go and fetch those too. It's easier to just use ?SingleWsdl.

Other than making sure your client certificate has a private key, everything looks correct to me.

As for your question about the VS WCF Connected Services tool, there's currently no capability to do this. You best option is what you're doing, download it yourself and then point the tooling at the file you downloaded. If you are going to automate some aspect of this, once you have the download part working, you can use the dotnet-svcutil nuget command line tool to generate the client.

TehWardy commented 2 weeks ago

It looks like the cer file i'm loading doesn't contain the private key. The API call to GetRSAPrivateKey returns null.

I looked at a bunch of the method calls on it ... private key found anywhere. My understanding is that that the cert is to be used as client cert to verify any calls I make are legit calls.

I'm a little confused by the provider process though as they had me generate a key and then csr which they returned a a cer file for.

Do I need to now add the private key on my end or something in order to use this (I wasn't provided this info in any docs)?

This is exactly what I think I need to do as there doesn't seem to be any UI for this, is this documented somewhere (specifically doing this with the client cert) ...

If you are going to automate some aspect of this, once you have the download part working, you can use the dotnet-svcutil nuget command line tool to generate the client.

mconnew commented 2 days ago

Apparently I forgot to hit the comment button on my reply and my response got lost to the great bit bucket in the sky. When you use the CSR mechanism, the private key is never sent, only the certificate that wraps the public key. The cer that's returned is the signed certificate holding the public key only. The location of the private key will depend on what tooling you used to create it and the subsequent csr file. If you used the Windows ecosystem, the private key will be in your certificate store along with the unsigned public key. You basically import the cer file which gets matched up with the private key, and you can export them together by specifying to export the private key at the same time in the Windows certificate manager. If you used OpenSsl to generate it, it's likely in a file on your local file system next to where the csr was created. You'll need to use an OpenSsl command line command to combine them.

TehWardy commented 16 hours ago

In case anyone cares / wants this in the future ... Using the cer file was my problem, I needed to use a PFX generated from it using the OpenSSL tooling. Once you have that you can do this ...

using System.Reflection;
using System.Security.Cryptography.X509Certificates;

public static class Wsdl
{
    public static async ValueTask<string> GetDescription(string wsdlUrl, string certResource, string password)
    {
        using var cert = GetCert(certResource, password);

        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(cert);

        using var client = new HttpClient(handler);
        return await client.GetStringAsync(wsdlUrl);
    }

    static X509Certificate2 GetCert(string certResource, string password)
    {
        Stream certStream = Assembly
            .GetExecutingAssembly()
            .GetManifestResourceStream(certResource);

        certStream.Seek(0, SeekOrigin.Begin);

        var memStream = new MemoryStream();
        certStream.CopyTo(memStream);
        return new(memStream.ToArray(), password);
    }
}

... Then came the hard part, "doing the client generation" ... svcutil is a fiddly tool but eventually with a little trial and error I got this ...

using System.Diagnostics;

public static class WsdlClientGenerator
{
    public static async Task<string> GenerateClientAsync(string wsdlDescription)
    {
        string tempPath = Path.GetTempPath();
        string wsdlFilePath = Path.Combine(tempPath, "service.wsdl");
        string outputDir = Path.Combine(tempPath, "GeneratedClient");

        await File.WriteAllTextAsync(wsdlFilePath, wsdlDescription);

        // Formulate the arguments directly using correct options
        string arguments = $"\"{wsdlFilePath}\" -d \"{outputDir}\" -n \"*,YourNamespace\"";

        ProcessStartInfo startInfo = new()
        {
            FileName = "dotnet-svcutil",
            Arguments = arguments,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
        };

        using Process process = new() { StartInfo = startInfo };

        process.Start();
        await process.WaitForExitAsync();

        string generatedFilePath = Path.Combine(outputDir, "Reference.cs");
        return await File.ReadAllTextAsync(generatedFilePath);
    }
}

... This works but I don't like that it dumps everything in a single file and in the temp folder, it's also not part of the IDE in such a way that I can just "right click -> update" but it does the job at least.

To use all this ...

var serviceRootUrl = "service url";
var certResource = "your.pfx";
var password = "your pfx password";

string description = await Wsdl
    .GetDescription($"{serviceRootUrl}?wsdl", certResource, password);

string clientCode = await WsdlClientGenerator.GenerateClientAsync(description);

Console.WriteLine(clientCode);
Console.ReadKey();

... i'd like to see support for this in VS if anyone on the VS team is willing to implement this. Also svcutil executed from the project folder could probably support a param or two for using a cert.