This commit is contained in:
master
2025-10-12 20:37:18 +03:00
parent 016c5a3fe7
commit d3a98326d1
306 changed files with 21409 additions and 4449 deletions

View File

@@ -0,0 +1,125 @@
using System;
using StellaOps.Authority.Plugin.Standard.Security;
using StellaOps.Cryptography;
namespace StellaOps.Authority.Plugin.Standard.Tests.Security;
public class CryptoPasswordHasherTests
{
[Fact]
public void Hash_EmitsArgon2idByDefault()
{
var options = CreateOptions();
var hasher = new CryptoPasswordHasher(options, new DefaultCryptoProvider());
var encoded = hasher.Hash("Secr3t!");
Assert.StartsWith("$argon2id$", encoded, StringComparison.Ordinal);
}
[Fact]
public void Verify_ReturnsSuccess_ForCurrentAlgorithm()
{
var options = CreateOptions();
var provider = new DefaultCryptoProvider();
var hasher = new CryptoPasswordHasher(options, provider);
var encoded = hasher.Hash("Passw0rd!");
var result = hasher.Verify("Passw0rd!", encoded);
Assert.Equal(PasswordVerificationResult.Success, result);
}
[Fact]
public void Verify_FlagsLegacyPbkdf2_ForRehash()
{
var options = CreateOptions();
var provider = new DefaultCryptoProvider();
var hasher = new CryptoPasswordHasher(options, provider);
var legacy = new Pbkdf2PasswordHasher().Hash(
"Passw0rd!",
new PasswordHashOptions
{
Algorithm = PasswordHashAlgorithm.Pbkdf2,
Iterations = 150_000
});
var result = hasher.Verify("Passw0rd!", legacy);
Assert.Equal(PasswordVerificationResult.SuccessRehashNeeded, result);
}
[Fact]
public void Verify_RejectsTamperedPayload()
{
var options = CreateOptions();
var provider = new DefaultCryptoProvider();
var hasher = new CryptoPasswordHasher(options, provider);
var legacy = new Pbkdf2PasswordHasher().Hash(
"Passw0rd!",
new PasswordHashOptions
{
Algorithm = PasswordHashAlgorithm.Pbkdf2,
Iterations = 160_000
});
var tampered = legacy + "corrupted";
var result = hasher.Verify("Passw0rd!", tampered);
Assert.Equal(PasswordVerificationResult.Failed, result);
}
[Fact]
public void Verify_AllowsLegacyAlgorithmWhenConfigured()
{
var options = CreateOptions();
options.PasswordHashing = options.PasswordHashing with
{
Algorithm = PasswordHashAlgorithm.Pbkdf2,
Iterations = 200_000
};
var provider = new DefaultCryptoProvider();
var hasher = new CryptoPasswordHasher(options, provider);
var legacy = new Pbkdf2PasswordHasher().Hash(
"Passw0rd!",
new PasswordHashOptions
{
Algorithm = PasswordHashAlgorithm.Pbkdf2,
Iterations = 200_000
});
var result = hasher.Verify("Passw0rd!", legacy);
Assert.Equal(PasswordVerificationResult.Success, result);
}
private static StandardPluginOptions CreateOptions() => new()
{
PasswordPolicy = new PasswordPolicyOptions
{
MinimumLength = 8,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
RequireSymbol = false
},
Lockout = new LockoutOptions
{
Enabled = true,
MaxAttempts = 5,
WindowMinutes = 15
},
PasswordHashing = new PasswordHashOptions
{
Algorithm = PasswordHashAlgorithm.Argon2id,
MemorySizeInKib = 8 * 1024,
Iterations = 2,
Parallelism = 1
}
};
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using StellaOps.Authority.Plugin.Standard;
using StellaOps.Cryptography;
namespace StellaOps.Authority.Plugin.Standard.Tests;
@@ -96,4 +97,49 @@ public class StandardPluginOptionsTests
Assert.Equal(Path.GetFullPath(absolute), options.TokenSigning.KeyDirectory);
}
[Fact]
public void Validate_Throws_WhenPasswordHashingMemoryInvalid()
{
var options = new StandardPluginOptions
{
PasswordHashing = new PasswordHashOptions
{
MemorySizeInKib = 0
}
};
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate("standard"));
Assert.Contains("memory", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_Throws_WhenPasswordHashingIterationsInvalid()
{
var options = new StandardPluginOptions
{
PasswordHashing = new PasswordHashOptions
{
Iterations = 0
}
};
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate("standard"));
Assert.Contains("iteration", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_Throws_WhenPasswordHashingParallelismInvalid()
{
var options = new StandardPluginOptions
{
PasswordHashing = new PasswordHashOptions
{
Parallelism = 0
}
};
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate("standard"));
Assert.Contains("parallelism", ex.Message, StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -34,6 +34,9 @@ public class StandardPluginRegistrarTests
["passwordPolicy:requireDigit"] = "false",
["passwordPolicy:requireSymbol"] = "false",
["lockout:enabled"] = "false",
["passwordHashing:memorySizeInKib"] = "8192",
["passwordHashing:iterations"] = "2",
["passwordHashing:parallelism"] = "1",
["bootstrapUser:username"] = "bootstrap",
["bootstrapUser:password"] = "Bootstrap1!",
["bootstrapUser:requirePasswordReset"] = "true"

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
@@ -7,6 +9,7 @@ using MongoDB.Driver;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugin.Standard.Security;
using StellaOps.Authority.Plugin.Standard.Storage;
using StellaOps.Cryptography;
namespace StellaOps.Authority.Plugin.Standard.Tests;
@@ -37,13 +40,21 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
Enabled = true,
MaxAttempts = 2,
WindowMinutes = 1
},
PasswordHashing = new PasswordHashOptions
{
Algorithm = PasswordHashAlgorithm.Argon2id,
MemorySizeInKib = 8 * 1024,
Iterations = 2,
Parallelism = 1
}
};
var cryptoProvider = new DefaultCryptoProvider();
store = new StandardUserCredentialStore(
"standard",
database,
options,
new Pbkdf2PasswordHasher(),
new CryptoPasswordHasher(options, cryptoProvider),
NullLogger<StandardUserCredentialStore>.Instance);
}
@@ -65,6 +76,7 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
var result = await store.VerifyPasswordAsync("alice", "Password1!", CancellationToken.None);
Assert.True(result.Succeeded);
Assert.Equal("alice", result.User?.Username);
Assert.Empty(result.AuditProperties);
}
[Fact]
@@ -90,6 +102,46 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
Assert.Equal(AuthorityCredentialFailureCode.LockedOut, second.FailureCode);
Assert.NotNull(second.RetryAfter);
Assert.True(second.RetryAfter.Value > System.TimeSpan.Zero);
Assert.Contains(second.AuditProperties, property => property.Name == "plugin.lockout_until");
}
[Fact]
public async Task VerifyPasswordAsync_RehashesLegacyHashesToArgon2()
{
var legacyHash = new Pbkdf2PasswordHasher().Hash(
"Legacy1!",
new PasswordHashOptions
{
Algorithm = PasswordHashAlgorithm.Pbkdf2,
Iterations = 160_000
});
var document = new StandardUserDocument
{
Username = "legacy",
NormalizedUsername = "legacy",
PasswordHash = legacyHash,
Roles = new List<string>(),
Attributes = new Dictionary<string, string?>(),
CreatedAt = DateTimeOffset.UtcNow.AddDays(-1),
UpdatedAt = DateTimeOffset.UtcNow.AddDays(-1)
};
await database.GetCollection<StandardUserDocument>("authority_users_standard")
.InsertOneAsync(document);
var result = await store.VerifyPasswordAsync("legacy", "Legacy1!", CancellationToken.None);
Assert.True(result.Succeeded);
Assert.Equal("legacy", result.User?.Username);
Assert.Contains(result.AuditProperties, property => property.Name == "plugin.rehashed");
var updated = await database.GetCollection<StandardUserDocument>("authority_users_standard")
.Find(u => u.NormalizedUsername == "legacy")
.FirstOrDefaultAsync();
Assert.NotNull(updated);
Assert.StartsWith("$argon2id$", updated!.PasswordHash, StringComparison.Ordinal);
}
public Task InitializeAsync() => Task.CompletedTask;