Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
For IdentityServer4's AddSigningCredentials in production

AddSigningCredentials

The RsaKeyService.cs file is inspired by this repo.

In Startup.cs of an IdentityServer4 app written for dotnet core 2.x, you'll see code like:

if (Environment.IsDevelopment())
{
    builder.AddDeveloperSigningCredential();
}
else
{
    throw new Exception("need to configure key material");
}

An Exception will be thrown in production, because you're expected to specify a more secure signing credential in production.

I don't fully understand how signing credentials are used, so I am open to simple explanations on the subject, but considering that I spent quite a while coming up with this way to generate signing credentials for production, I thought to share.

Please give feedback if you know a better way, and be kind enough to explain why.


You can register the RsaKeyService class as a Singleton in your Startup.cs file like:

public void ConfigureServices(IServiceCollection services)
{
  var rsa = new RsaKeyService(Environment, TimeSpan.FromDays(30));
  services.AddSingleton<RsaKeyService>(provider => rsa);
}

This makes sure that at least 30 days passes before a new RSA key file is generated.

Next, rewrite the signing credential conditional snippet as:

if (Environment.IsDevelopment())
{
    builder.AddDeveloperSigningCredential();
}
else
{
    builder.AddSigningCredential(rsa.GetKey());
}

Holla in the comments, if this helps you.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace Mykeels.Services
{
public class RsaKeyService
{
/// <summary>
/// This points to a JSON file in the format:
/// {
/// "Modulus": "",
/// "Exponent": "",
/// "P": "",
/// "Q": "",
/// "DP": "",
/// "DQ": "",
/// "InverseQ": "",
/// "D": ""
/// }
/// </summary>
private string _file {
get {
return Path.Combine(_environment.ContentRootPath, "rsakey.json");
}
}
private readonly IHostingEnvironment _environment;
private readonly TimeSpan _timeSpan;
public RsaKeyService(IHostingEnvironment environment, TimeSpan timeSpan) {
_environment = environment;
_timeSpan = timeSpan;
}
public bool NeedsUpdate() {
if (File.Exists(_file)) {
var creationDate = File.GetCreationTime(_file);
return DateTime.Now.Subtract(creationDate) > _timeSpan;
}
return true;
}
public RSAParameters GetRandomKey()
{
using (var rsa = new RSACryptoServiceProvider(2048))
{
try
{
return rsa.ExportParameters(true);
}
finally
{
rsa.PersistKeyInCsp = false;
}
}
}
public RsaKeyService GenerateKeyAndSave(bool forceUpdate = false)
{
if (forceUpdate || NeedsUpdate()) {
var p = GetRandomKey();
RSAParametersWithPrivate t = new RSAParametersWithPrivate();
t.SetParameters(p);
File.WriteAllText(_file, JsonConvert.SerializeObject(t, Formatting.Indented));
}
return this;
}
/// <summary>
///
/// Generate
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
public RSAParameters GetKeyParameters()
{
if (!File.Exists(_file)) throw new FileNotFoundException("Check configuration - cannot find auth key file: " + _file);
var keyParams = JsonConvert.DeserializeObject<RSAParametersWithPrivate>(File.ReadAllText(_file));
return keyParams.ToRSAParameters();
}
public RsaSecurityKey GetKey() {
if (NeedsUpdate()) GenerateKeyAndSave();
var provider = new System.Security.Cryptography.RSACryptoServiceProvider();
provider.ImportParameters(GetKeyParameters());
return new RsaSecurityKey(provider);
}
/// <summary>
/// Util class to allow restoring RSA parameters from JSON as the normal
/// RSA parameters class won't restore private key info.
/// </summary>
private class RSAParametersWithPrivate
{
public byte[] D { get; set; }
public byte[] DP { get; set; }
public byte[] DQ { get; set; }
public byte[] Exponent { get; set; }
public byte[] InverseQ { get; set; }
public byte[] Modulus { get; set; }
public byte[] P { get; set; }
public byte[] Q { get; set; }
public void SetParameters(RSAParameters p)
{
D = p.D;
DP = p.DP;
DQ = p.DQ;
Exponent = p.Exponent;
InverseQ = p.InverseQ;
Modulus = p.Modulus;
P = p.P;
Q = p.Q;
}
public RSAParameters ToRSAParameters()
{
return new RSAParameters()
{
D = this.D,
DP = this.DP,
DQ = this.DQ,
Exponent = this.Exponent,
InverseQ = this.InverseQ,
Modulus = this.Modulus,
P = this.P,
Q = this.Q
};
}
}
}
}
@pseudoramble

This comment has been minimized.

Copy link

@pseudoramble pseudoramble commented Aug 29, 2019

I have a question about rotating the signing key. Does this code force IdentityServer4 to use the new signing key when it's generated? If it does, how do you ensure that both previously signed tokens keep working until they expire? If it doesn't, do you simply reload the new key each time the app is restarted?

@mykeels

This comment has been minimized.

Copy link
Owner Author

@mykeels mykeels commented Aug 29, 2019

I did some reading, and rotating keys helps keep your system secure. See this article and github issue:

The article helped give credence to key rotation, and the github issue discusses notifying consumers of the identity server with http headers about the lifetime of the current key.

@rodrigolira

This comment has been minimized.

Copy link

@rodrigolira rodrigolira commented Oct 21, 2019

@mykeels by instantiating the RsaKeyService like that, wouldn't it cause issues if you want to scale and run multiple instances of the IdentityServer?

@mykeels

This comment has been minimized.

Copy link
Owner Author

@mykeels mykeels commented Oct 22, 2019

Probably.

I no longer use this method, because I couldn't figure out how to set it up in dotnet 3.0.

I use X509Certificates instead. I'm sure you could setup a shared certificate for your use case. @rodrigolira

@Karql

This comment has been minimized.

Copy link

@Karql Karql commented Jan 21, 2020

@mykeels Hi could you share solution with X509Certificates? ;)

@mykeels

This comment has been minimized.

Copy link
Owner Author

@mykeels mykeels commented Jan 21, 2020

I use a bash script like:

#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"

PRIVATE_PEM=$DIR/private.pem
PUBLIC_PEM=$DIR/public.pem
PFX=$DIR/mycert.pfx
PASSWD=$1

if [ -z "$PASSWD" ]
then
    PASSWD="HereIsMyDefaultPassword"
fi

echo "Creating Private Key"
openssl genrsa 2048 > $PRIVATE_PEM

echo "Creating Public Key"
echo """<Country-Code>
<State>
<City>
<Company-Name>
Dev
<Email-Address>
""" | openssl req -x509 -days 1000 -new -key $PRIVATE_PEM -out $PUBLIC_PEM

echo ""
echo "Creating Certificate"

openssl pkcs12 -export -in $PUBLIC_PEM -inkey $PRIVATE_PEM -out $PFX -password pass:$PASSWD

to generate the certificate. Be sure to replace <Country-Code> and others, appropriately.

@mykeels

This comment has been minimized.

Copy link
Owner Author

@mykeels mykeels commented Jan 21, 2020

In your Startup.cs, you can then have:

if (Environment.IsTestOrDevelopment())
{
  builder.AddDeveloperSigningCredential();
}
else
{
  string password = Configuration["Jwt:Secret"];
  Debug.Assert(!String.IsNullOrEmpty(password), "Jwt:Secret is missing from appsettings");
  string certificate = Configuration["Jwt:Certificate"];
  Debug.Assert(!String.IsNullOrEmpty(certificate), "Jwt:Certificate is missing from appsettings");
  
  var cert = new X509Certificate2(
    certificate,
    password,
    X509KeyStorageFlags.MachineKeySet |
    X509KeyStorageFlags.PersistKeySet |
    X509KeyStorageFlags.Exportable
  );
  builder.AddSigningCredential(cert);
}

The Jwt:Secret config variable can be any passphrase such as: HereIsMyDefaultPassword. The Jwt:Certificate config variable should be the file path of the generated*.pfxcertificate file. e.g.C:\Dev\Project\mycert.pfx`

NB: Environment.IsTestOrDevelopment() is an extension method, so you may want to customise this for your use-case.

@Karql

This comment has been minimized.

Copy link

@Karql Karql commented Jan 21, 2020

@mykeels Thank you!

Best regards,
Mateusz

@MrTomZed

This comment has been minimized.

Copy link

@MrTomZed MrTomZed commented Oct 1, 2020

Example code is not working. I had to add RsaSigningAlgorithm as a parameter:

builder.AddSigningCredential(rsa.GetKey(), RsaSigningAlgorithm.RS512);

@jcmontx

This comment has been minimized.

Copy link

@jcmontx jcmontx commented Dec 31, 2020

You're a hero. Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment