Kestrel isn't "using" pfx file for https - c#

My local development is wanting to install a localhost ssl certificate even though I have specified a pfx I want it to use.
core API .NET 5.0
My understanding was that kestrel was used by default so this is what I have done.
In my hosts file I setup a FQDN.
127.0.0.1 myapi.mysite.com
In my program.cs I added ConfigureKestrel
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
options.Listen(IPAddress.Loopback, 44394, listenOptions =>
{
var serverCertificate = LoadCertificate();
listenOptions.UseHttps(serverCertificate); // <- Configures SSL
});
});
});
private static X509Certificate2 LoadCertificate()
{
var assembly = typeof(Startup).GetTypeInfo().Assembly;
var embeddedFileProvider = new EmbeddedFileProvider(assembly, "My.API");
var certificateFileInfo = embeddedFileProvider.GetFileInfo("wildcard_mydomain_com.pfx");
using (var certificateStream = certificateFileInfo.CreateReadStream())
{
byte[] certificatePayload;
using (var memoryStream = new MemoryStream())
{
certificateStream.CopyTo(memoryStream);
certificatePayload = memoryStream.ToArray();
}
return new X509Certificate2(certificatePayload, "password");
}
}
I do not get any errors but I don't get the custom domain certificate presented to the browser either. The browser tell me I am unsecure and wants me to accept the localhost cert....which I don't want to accept. I want my custom domain cert to be used.
Am I not understanding how the certs work in a browser?
Doesn't the web server present the cert to the browser and the browser checks to see if it is issued by a legit CA?
Am I loading my custom cert but not "attaching" it to the development server to use?

Related

Keeping Track of X509 Cert in dotnet core

I currently have a DotNet Core app that's requiring a users x509 Certificate. I currently can pull it and validate it with the following
In my Program.cs
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});
});
In my Startup.cs
services.AddScoped<CertificateValidationService>();
services.AddControllers();
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnAuthenticationFailed = context =>
{
context.NoResult();
context.Response.Headers.Add("Token-Expired", "true");
context.Response.ContentType = "text/plain";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
},
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices
.GetRequiredService<CertificateValidationService>();
if (validationService.ValidateCertificate(
context.ClientCertificate))
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer)
};
context.Properties.SetParameter("x509", context.ClientCertificate);
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail($"Unrecognized client certificate: " +
$"{context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false)}");
}
return Task.CompletedTask;
}
};
});
services.AddControllers();
I also have my Implementation for verifying the cert and that works fine. However, later in the application I want to do the following
[Route("/signature")]
[HttpGet]
public string Signature()
{
using (var signer = new PdfDocumentSigner(#"C:\test\Document.pdf"))
{
ITsaClient tsaClient = new TsaClient(new Uri(#"https://freetsa.org/tsr"),
DevExpress.Office.DigitalSignatures.HashAlgorithmType.SHA256);
string signatureName = signer.GetSignatureFieldNames(false)[0];
// Create a provider that retrieves certificates from a store:
// public CertificateStoreProvider(X509Certificate2Collection collection);
using (var certificateStoreProvider =
new CertificateStoreProvider(new X509Store(StoreLocation.CurrentUser), true))
{
// Add the signature to the security store
// and specify the CrlClient and OcspClient objects
// used to check the certificates' revocation status:
signer.AddToDss(signatureName, new CrlClient(), new OcspClient(), certificateStoreProvider);
}
signer.SaveDocument(#"C:\test\signedLTV.pdf", new[] { new PdfSignatureBuilder(new PdfTimeStamp(tsaClient)) });
}
return "The file was signed";
}
Now obviously the problem is that this is the X509 Certificate on my Local Host. BUT what I want to do is use the one that the user validated with which was passed along in the Program/Startup
Is there a way I can programmatically get that or pass that along safely?
You can't do this. In order to use client certificate for subsequent cryptographic operations (like signing or decryption), you need a client's private key. However, client never sends its private key. Client's private key must never leave client machine. Your entire approach is non-working. Here is the relevant thread with a bit deeper explanation: Open X509 Certificates Selection Using USB Token in C# Hosted on IIS
What you can do is to move signing process to client side (execute in browser), but this approach has its own issues, like you will have to download entire PDF to client browser, sign somehow and then upload back to server.

Client Certificate - 403 - Forbidden: Access is denied

When a third party tries to call my API endpoint with the certificate in .cer format, which I exported from the .pfx file and sent to them.
They will get 403 - Forbidden: Access is denied. You do not have permission to view this directory or page using the credentials that you supplied.
I investigate what could be caused this problem.
When i Install/import Certificate in .cer format in certificate store under Personal and than i try to call my endpoint i can see my certificate is NOT on list of certificates and than i hit ok button i will get 403 - Forbidden: Access is denied.
BUT
When i Install/import Certificate in .pfx format with passepharse in certificate store under Personal and then i try to call my endpoint on browser and also with postman THIS time i can see my certificate in the list of certificates on browser and than i choose the certificate and hit button i will successfully coming into directory and i also get 200 ok response in postman with ofcourse add certificate in .pfx format in postman.
And im confused now 3rd party only accept Client Certificate in .cer format and as i understand .pfx is for inside organization and not for outside organization.
** I should note my Client Certificate contains no Private Key its only contains Public key.
** I'm sure All configuration on server and IIS it is correct.
** I'm not sure how to add private key into my Client certificate with .cer format! or should i ?!
Did i missed something here ! its been 4 days working on this but still no luck :(
Can anyone please help me or point me into right direction! thanks :)
This is how i get Client certificate in ASP.NET Core 3.1:
MyCertificateValidationService.cs
I compared the client certificate I have with the client certificate I get from the request:
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
try
{
var _path = #"c:\temp\ClientCertification.cer";
var cert2 = new X509Certificate2(File.ReadAllBytes(_path));
if (clientCertificate.Thumbprint == cert2.Thumbprint)
{
return true;
}
}
catch (System.Exception)
{
throw;
}
return false;
}
My API Endpoint:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[Consumes("application/xml")]
[Produces("application/xml")]
[ProducesResponseType(typeof(DespatchAdvice), (int)HttpStatusCode.OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesDefaultResponseType]
[HttpPost("SendDespatch")]
public IActionResult SendDespatch([FromBody] DespatchAdvice despatches)
{
//do something
}
}
Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<MyCertificateValidationService>();
services.AddSingleton<MailHandler>();
services.AddScoped<IDespatch, DespatchRepo>();
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options => // code from ASP.NET Core sample
{
// https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth
options.AllowedCertificateTypes = CertificateTypes.All;
//options.RevocationMode = X509RevocationMode.NoCheck;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = StringToByteArray(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
services.AddControllers().AddXmlSerializerFormatters();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private static byte[] StringToByteArray(string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
Program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
=> WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
var cert = new X509Certificate2(Path.Combine("cert.pfx"), "password");
options.ConfigureHttpsDefaults(o =>
{
o.ServerCertificate = cert;
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
})
.Build();
}
Let's clarify how SSL client certificate authentication works. Below I assume that "certificate" never contains private key, only public key.
Client provides a certificate he owns during SSL handshake. Server validates that this certificate matches some arbitrary criterias, for example server might require that it was issued by certain certification authority, or, like in your case - that this is a specific certificate (it's thumbprint matches what you expect).
Now, client must prove to server that he actually owns private key for this certificate, in simple terms by signing some info with that private key, and server then verifies that with client's certificate (sent before, as described above) public key.
If client successfully proves he owns private key for given certificate, AND that certificate matches server's criterias - then client is authenticated and can proceed.
You can already see why your current approach cannot work - .cer file you are sending to client does not contain private key, so it cannot be used for authentication purposes.
Now you have two ways:
1 - YOU generate fresh certificate, and send both certificate and private key to your client. Variation of this is when you create your own certification authority and then issue such client certificate under that authority. Then in validation code you can just ensure that certificate was issued by your authority instead of direct comparision of thumbprints. This is reasonable way if you have thousands of clients.
Drawback of such approach is that you now have (or had at one point) secure information you do not need - that is private key of certificate issued for your client. If unauthorized access had happened using that client's private key (client had his private key stolen) - client in theory can claim that YOU leaked this key. Another drawback is that you have to pass sensitive data (private key) over some, preferrably secure channel. If you just email private key to the client - anything bad can happen (like client won't delete it, then later his email is hacked and key leaks to the hacker).
2 - YOUR CLIENT generates certificate and private key. This is the best way in case you have not much clients. Client stores private key for himself and sends you certificate (say in .cer format) which does not contain private key. Now he authenticates as described above, and you just validate that certificate provided in SSL handshake matches certificate sent to you by client beforehand (like you are doing now, by comparing thumbprint). Asp.net then ensures that client has matching private key for this certificate.
Now, no sensitive data has to be sent anywhere, and in case client leaks his private key - you cannot be responsible for that since you never ever had this key in the first place.
Side note: if you are going 1st route by generating certificate for your client - note that it's a fresh certificate, completely unrelated to your server certificate. Your server certificate private key should of course not be ever sent anywhere. That's related to your comment "but it’s not dangerous to send pfx to Them ?!". No, because you just generated this pfx specifically for that client.

how to use ssl certificates with gRPC and ASP Net Core 3.0?

I am rtying to configure the service to use a SSL certificate. I have read this post:
How to enable server side SSL for gRPC?
I guess this is the main code:
var cacert = File.ReadAllText(#"ca.crt");
var servercert = File.ReadAllText(#"server.crt");
var serverkey = File.ReadAllText(#"server.key");
var keypair = new KeyCertificatePair(servercert, serverkey);
var sslCredentials = new SslServerCredentials(new List<KeyCertificatePair>() { keypair }, cacert, false);
var server = new Server
{
Services = { GrpcTest.BindService(new GrpcTestImpl(writeToDisk)) },
Ports = { new ServerPort("0.0.0.0", 555, sslCredentials) }
};
server.Start();
The problem is that in my case, I don't start the service in this way, I am using kestrel, and the code is this:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
System.Net.IPAddress miAddress = System.Net.IPAddress.Parse("x.x.x.x");
//options.Listen(miAddress, 5001, o => o.Protocols = HttpProtocols.Http2);
options.Listen(miAddress, 5001, l =>
{
l.Protocols = HttpProtocols.Http2;
l.UseHttps();
});
});
webBuilder.UseStartup<Startup>();
});
In this case, I don't have access to SslCredentials, so I can't create a new one.
How could I configure my ssl certificate using kestrel?
Thanks.
The post you linked to is for Grpc.Core, the grpc-dotnet implementation is configured differently.
This documentation and example should help:
https://github.com/grpc/grpc-dotnet/blob/dd72d6a38ab2984fd224aa8ed53686dc0153b9da/testassets/InteropTestsWebsite/Program.cs#L55
https://learn.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz?view=aspnetcore-3.1
(in another words, you can configure the certificates on the server side exactly the same way as you would for any other HTTP/2 server - there's nothing grpc specific in configuring the secure connections in ASP.NET Core).
It looks like you mistake authentication by certificates for SSL-data-encryption. In case you just want to encrypt the data channel, good practice is to use Kestrel:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.ConfigureKestrel(options =>
{
options.Listen(IPAddress.Loopback, 5005, configure => { configure.UseHttps(); configure.Protocols = HttpProtocols.Http2; });
});
});
The call to UseHttps() uses the internal ASP.NET Core’s trusted development certificate.
If you want to provide one yourself, use i.e. (or the other overloads):
public static ListenOptions UseHttps(this ListenOptions listenOptions, X509Certificate2 serverCertificate)

Make Http.sys work with Windows Authentication

I try to make Window authentication work with Kestrel by following the links:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys?view=aspnetcore-3.0#how-to-use-httpsys
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-3.0&tabs=visual-studio#httpsys
Here is the code.
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseHttpSys(options =>
{
options.AllowSynchronousIO = true;
options.Authentication.Schemes = AuthenticationSchemes.None;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("https://localhost:8080");
});
webBuilder.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.Authentication.Schemes =
AuthenticationSchemes.NTLM |
AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
});
});
However, browse https://localhost:8080 shows the following error message (Edge)?
Can’t connect securely to this page
This might be because the site uses outdated or unsafe TLS security settings. If this keeps happening, try contacting the website’s owner.
It's because you didn't have development certification installed on your machine.
try this:
dotnet dev-certs https --trust

How to enable certificate authentication in App-Service with .NET Core 3.0?

I am implementing client authentication with certificates to access my API. I followed the documentation from Microsoft https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0
My problem is, that I never receive an client cert. I tested it locally and on Azure as well.
I have tried several variations, but with same result.
Excerpst from my Startup class:
// Register Certificate Authentication
//services.AddCertificateForwarding(options => options.CertificateHeader = "X-ARR-ClientCert");
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = System.Text.Encoding.ASCII.GetBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
services.AddSingleton<CertificateValidationService>();
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices.GetService<CertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Fail("invalid cert");
return Task.CompletedTask;
}
};
});
and
app.UseCertificateForwarding();
app.UseAuthentication();
in Program.cs I added this config:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(opt =>
{
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
});
Diagnostic
If you enable trace logs in console - you could see additional info why this certificate forwarding middleware won't work.
One important thing about CertificateForwarding middleware is it only works with HTTPS schema. So you should use one of two options here:
Use HTTPS on application level (Enable HTTPS in Kestrel)
This way is a plenty easy so it should not be a problem for you to enable it. Just follow official instructions.
Use schema forwarding
If you use proxy server, like NGINX before application, that works using HTTPS and proxying requests to your application through HTTP - you could face issue here that middeware methods not being called.
To fix that, there is one thing you should add is to proxy schema through NGINX this way:
# nginx.conf
proxy_set_header X-Forwarded-Proto $scheme;
// Configure startup
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
});
// and use it
app.UseForwardedHeaders(); // Place it before UseCertificateForwarding()
This way, when your request goes to NGINX through HTTPS nginx will proxy it to your application through HTTP but with X-Forwarded-Proto header that contains HTTPS schema. So your dotnet application will launch CertificateForwarding middleware correctly.
Remove the header converter when calling AddCertificateForwarding

Categories

Resources