Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/thread safe runtime sign cred modification #1

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .idea/.idea.OpenIddict/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.idea.OpenIddict/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.OpenIddict/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.OpenIddict/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.OpenIddict/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 4 additions & 7 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
{
"sdk": {
"version": "9.0.100",
"allowPrerelease": true,
"rollForward": "major"
"version": "9.0.0",
"rollForward": "major",
"allowPrerelease": true
},

"tools": {
"dotnet": "9.0.100",

"runtimes": {
"aspnetcore": [
"6.0.36",
"8.0.11"
]
}
},

"msbuild-sdks": {
"Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.23516.4",
"Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.23516.4",
"MSBuild.Sdk.Extras": "3.0.44",
"MSBuild.SDK.SystemWeb": "4.0.88"
}
}
}
16 changes: 16 additions & 0 deletions src/OpenIddict.Server/IOpenIddictCredentialList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.IdentityModel.Tokens;

namespace OpenIddict.Server
{
public interface IOpenIddictCredentialList<T> : IEnumerable<T>
{
public abstract SigningCredentials this[int index] { get; }
public abstract bool TrueForAll(Predicate<T> match);
public abstract bool Exists(Predicate<T> match);
public abstract T? Find(Predicate<T> match);
public abstract void Add(T item);
public abstract void AddRange(IEnumerable<T> items);
public abstract void Remove(string keyId);
public abstract void Clear();
}
}
78 changes: 78 additions & 0 deletions src/OpenIddict.Server/OpenIddictSecurityKeyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Diagnostics;
using Microsoft.IdentityModel.Tokens;

namespace OpenIddict.Server
{
public static class OpenIddictSecurityKeyExtensions
{
public static int Compare(SecurityKey left, SecurityKey right, DateTime now) => (left, right) switch
{
// If the two keys refer to the same instances, return 0.
({ } first, { } second) when ReferenceEquals(first, second) => 0,

// If one of the keys is a symmetric key, prefer it to the other one.
(SymmetricSecurityKey, SymmetricSecurityKey) => 0,
(SymmetricSecurityKey, not SymmetricSecurityKey) => -1,
(not SymmetricSecurityKey, SymmetricSecurityKey) => 1,

// If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet.
(X509SecurityKey first, not null) when first.Certificate.NotBefore > now => 1,
(not null, X509SecurityKey second) when second.Certificate.NotBefore > now => -1,

// If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date.
(X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter),

// If one of the keys is backed by a X.509 certificate, prefer the X.509 security key.
(X509SecurityKey, not X509SecurityKey) => -1,
(not X509SecurityKey, X509SecurityKey) => 1,

// If the two keys are not backed by a X.509 certificate, none should be preferred to the other.
(not X509SecurityKey, not X509SecurityKey) => 0
};

internal static string? GetKeyIdentifier(SecurityKey key)
{
// When no key identifier can be retrieved from the security keys, a value is automatically
// inferred from the hexadecimal representation of the certificate thumbprint (SHA-1)
// when the key is bound to a X.509 certificate or from the public part of the signing key.

if (key is X509SecurityKey x509SecurityKey)
{
return x509SecurityKey.Certificate.Thumbprint;
}

if (key is RsaSecurityKey rsaSecurityKey)
{
// Note: if the RSA parameters are not attached to the signing key,
// extract them by calling ExportParameters on the RSA instance.
var parameters = rsaSecurityKey.Parameters;
if (parameters.Modulus is null)
{
parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false);

Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003));
}

// Only use the 40 first chars of the base64url-encoded modulus.
var identifier = Base64UrlEncoder.Encode(parameters.Modulus);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
}

#if SUPPORTS_ECDSA
if (key is ECDsaSecurityKey ecsdaSecurityKey)
{
// Extract the ECDSA parameters from the signing credentials.
var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false);

Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));

// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
#endif

return null;
}
}
}
82 changes: 6 additions & 76 deletions src/OpenIddict.Server/OpenIddictServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,15 @@ options.RevocationEndpointUris.Count is not 0 ||
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0085));
}

if (!options.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0086));
}

var now = (
#if SUPPORTS_TIME_PROVIDER
options.TimeProvider?.GetUtcNow() ??
options.TimeProvider?.GetUtcNow().DateTime ??
#endif
DateTimeOffset.UtcNow
)
Expand All @@ -214,7 +214,7 @@ options.RevocationEndpointUris.Count is not 0 ||

// If all the registered signing credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.SigningCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
(x509SecurityKey.Certificate.NotBefore > now || x509SecurityKey.Certificate.NotAfter < now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0088));
}
Expand Down Expand Up @@ -390,15 +390,15 @@ descriptor.Type is OpenIddictServerHandlerType.Custom &&
options.Handlers.Sort(static (left, right) => left.Order.CompareTo(right.Order));

// Sort the encryption and signing credentials.
options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key, now));
options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key, now));
options.EncryptionCredentials.Sort((left, right) => OpenIddictSecurityKeyExtensions.Compare(left.Key, right.Key, now));
// options.SigningCredentials.Sort((left, right) => OpenIddictServerOptionsExtensions.Compare(left.Key, right.Key, now));

// Generate a key identifier for the encryption/signing keys that don't already have one.
foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key)
.Concat(options.SigningCredentials.Select(credentials => credentials.Key))
.Where(key => string.IsNullOrEmpty(key.KeyId)))
{
key.KeyId = GetKeyIdentifier(key);
key.KeyId = OpenIddictSecurityKeyExtensions.GetKeyIdentifier(key);
}

// Attach the signing credentials to the token validation parameters.
Expand All @@ -410,75 +410,5 @@ from credentials in options.SigningCredentials
options.TokenValidationParameters.TokenDecryptionKeys =
from credentials in options.EncryptionCredentials
select credentials.Key;

static int Compare(SecurityKey left, SecurityKey right, DateTime now) => (left, right) switch
{
// If the two keys refer to the same instances, return 0.
(SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0,

// If one of the keys is a symmetric key, prefer it to the other one.
(SymmetricSecurityKey, SymmetricSecurityKey) => 0,
(SymmetricSecurityKey, SecurityKey) => -1,
(SecurityKey, SymmetricSecurityKey) => 1,

// If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet.
(X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > now => 1,
(SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > now => -1,

// If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date.
(X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter),

// If one of the keys is backed by a X.509 certificate, prefer the X.509 security key.
(X509SecurityKey, SecurityKey) => -1,
(SecurityKey, X509SecurityKey) => 1,

// If the two keys are not backed by a X.509 certificate, none should be preferred to the other.
(SecurityKey, SecurityKey) => 0
};

static string? GetKeyIdentifier(SecurityKey key)
{
// When no key identifier can be retrieved from the security keys, a value is automatically
// inferred from the hexadecimal representation of the certificate thumbprint (SHA-1)
// when the key is bound to a X.509 certificate or from the public part of the signing key.

if (key is X509SecurityKey x509SecurityKey)
{
return x509SecurityKey.Certificate.Thumbprint;
}

if (key is RsaSecurityKey rsaSecurityKey)
{
// Note: if the RSA parameters are not attached to the signing key,
// extract them by calling ExportParameters on the RSA instance.
var parameters = rsaSecurityKey.Parameters;
if (parameters.Modulus is null)
{
parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false);

Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003));
}

// Only use the 40 first chars of the base64url-encoded modulus.
var identifier = Base64UrlEncoder.Encode(parameters.Modulus);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
}

#if SUPPORTS_ECDSA
if (key is ECDsaSecurityKey ecsdaSecurityKey)
{
// Extract the ECDSA parameters from the signing credentials.
var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false);

Debug.Assert(parameters.Q.X is not null, SR.GetResourceString(SR.ID4004));

// Only use the 40 first chars of the base64url-encoded X coordinate.
var identifier = Base64UrlEncoder.Encode(parameters.Q.X);
return identifier[..Math.Min(identifier.Length, 40)].ToUpperInvariant();
}
#endif

return null;
}
}
}
6 changes: 3 additions & 3 deletions src/OpenIddict.Server/OpenIddictServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ public sealed class OpenIddictServerOptions
/// <item><description>X.509 keys are always preferred to non-X.509 asymmetric keys.</description></item>
/// <item><description>X.509 keys with the furthest expiration date are preferred.</description></item>
/// <item><description>X.509 keys whose backing certificate is not yet valid are never preferred.</description></item>
/// </list>
/// </list>n
/// </remarks>
public List<SigningCredentials> SigningCredentials { get; } = [];
public IOpenIddictCredentialList<SigningCredentials> SigningCredentials { get; } = new OpenIddictSigningCredentialList([], null);

/// <summary>
/// Gets the absolute and relative URIs associated to the authorization endpoint.
Expand Down Expand Up @@ -170,7 +170,7 @@ public sealed class OpenIddictServerOptions
},
// Note: audience and lifetime are manually validated by OpenIddict itself.
ValidateAudience = false,
ValidateLifetime = false
ValidateLifetime = false,
};

/// <summary>
Expand Down
Loading