fix(inbound-api): resolve InboundAPI-001/003/005 — concurrent handler cache, constant-time API key compare, script trust-model enforcement

This commit is contained in:
Joseph Doherty
2026-05-16 19:47:17 -04:00
parent d30ded7e72
commit 6f4efdfa2e
6 changed files with 393 additions and 28 deletions

View File

@@ -37,7 +37,7 @@ public class ApiKeyValidatorTests
[Fact]
public async Task InvalidApiKey_Returns401()
{
_repository.GetApiKeyByValueAsync("bad-key").Returns((ApiKey?)null);
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey>());
var result = await _validator.ValidateAsync("bad-key", "testMethod");
Assert.False(result.IsValid);
@@ -48,7 +48,7 @@ public class ApiKeyValidatorTests
public async Task DisabledApiKey_Returns401()
{
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = false };
_repository.GetApiKeyByValueAsync("valid-key").Returns(key);
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
var result = await _validator.ValidateAsync("valid-key", "testMethod");
Assert.False(result.IsValid);
@@ -59,7 +59,7 @@ public class ApiKeyValidatorTests
public async Task ValidKey_MethodNotFound_Returns400()
{
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
_repository.GetApiKeyByValueAsync("valid-key").Returns(key);
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
_repository.GetMethodByNameAsync("nonExistent").Returns((ApiMethod?)null);
var result = await _validator.ValidateAsync("valid-key", "nonExistent");
@@ -73,7 +73,7 @@ public class ApiKeyValidatorTests
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
var method = new ApiMethod("testMethod", "return 1;") { Id = 10 };
_repository.GetApiKeyByValueAsync("valid-key").Returns(key);
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
_repository.GetMethodByNameAsync("testMethod").Returns(method);
_repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey>());
@@ -88,7 +88,7 @@ public class ApiKeyValidatorTests
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
var method = new ApiMethod("testMethod", "return 1;") { Id = 10 };
_repository.GetApiKeyByValueAsync("valid-key").Returns(key);
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
_repository.GetMethodByNameAsync("testMethod").Returns(method);
_repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey> { key });
@@ -98,4 +98,50 @@ public class ApiKeyValidatorTests
Assert.Equal(key, result.ApiKey);
Assert.Equal(method, result.Method);
}
// --- InboundAPI-003: API key must not be matched with a non-constant-time
// (timing-oracle) secret-equality lookup. ---
[Fact]
public async Task ValidateAsync_DoesNotUseSecretEqualityLookup()
{
// GetApiKeyByValueAsync translates to a SQL "WHERE KeyValue = @secret" early-exit
// comparison — a timing side-channel. The validator must not call it.
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
var method = new ApiMethod("testMethod", "return 1;") { Id = 10 };
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
_repository.GetMethodByNameAsync("testMethod").Returns(method);
_repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey> { key });
await _validator.ValidateAsync("valid-key", "testMethod");
await _repository.DidNotReceive()
.GetApiKeyByValueAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task ValidateAsync_WrongKey_ConstantTimePath_Returns401()
{
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
var result = await _validator.ValidateAsync("wrong-key", "testMethod");
Assert.False(result.IsValid);
Assert.Equal(401, result.StatusCode);
}
[Fact]
public async Task ValidateAsync_KeyOfDifferentLength_Returns401()
{
// FixedTimeEquals over UTF-8 bytes must reject length mismatches without leaking.
var key = new ApiKey("test", "valid-key") { Id = 1, IsEnabled = true };
_repository.GetAllApiKeysAsync().Returns(new List<ApiKey> { key });
var result = await _validator.ValidateAsync("valid-key-with-extra", "testMethod");
Assert.False(result.IsValid);
Assert.Equal(401, result.StatusCode);
}
}