├── src ├── README.md ├── AzureKeyVaultEmulator.Shared │ ├── Models │ │ ├── Keys │ │ │ ├── KeyAttributes.cs │ │ │ ├── Requests │ │ │ │ ├── RandomBytesRequest.cs │ │ │ │ ├── SignKeyRequest.cs │ │ │ │ ├── ReleaseKeyRequest.cs │ │ │ │ ├── VerifyHashRequest.cs │ │ │ │ ├── UpdateKeyRequest.cs │ │ │ │ └── ImportKeyRequest.cs │ │ │ ├── DeletedKeyBundle.cs │ │ │ ├── KeyOperationResult.cs │ │ │ ├── KeyOperationParameters.cs │ │ │ ├── KeyReleasePolicy.cs │ │ │ ├── KeyItemBundle.cs │ │ │ ├── CreateKey.cs │ │ │ ├── KeyProperties.cs │ │ │ ├── KeyRotationPolicy.cs │ │ │ ├── KeyReleaseVM.cs │ │ │ └── KeyBundle.cs │ │ ├── Secrets │ │ │ ├── SecretAttributes.cs │ │ │ ├── ListResult.cs │ │ │ ├── DeletedSecretBundle.cs │ │ │ ├── Requests │ │ │ │ ├── UpdateSecretRequest.cs │ │ │ │ └── SetSecretRequest.cs │ │ │ ├── SecretProperties.cs │ │ │ ├── SecretItemBundle.cs │ │ │ └── SecretBundle.cs │ │ ├── IAttributedModel.cs │ │ ├── ICreateItem.cs │ │ ├── ValueResponse.cs │ │ ├── Certificates │ │ │ ├── CertificateAttributes.cs │ │ │ ├── Requests │ │ │ │ ├── SetContactsRequest.cs │ │ │ │ ├── UpdateCertificateRequest.cs │ │ │ │ ├── MergeCertificatesRequest.cs │ │ │ │ ├── CreateCertificateRequest.cs │ │ │ │ ├── ImportCertificateRequest.cs │ │ │ │ └── CertificateContacts.cs │ │ │ ├── CertificateVersionItem.cs │ │ │ ├── KeyVaultContact.cs │ │ │ ├── CertificateProperties.cs │ │ │ ├── CertificateOperation.cs │ │ │ ├── CertificatePolicy.cs │ │ │ ├── X509CertificateProperties.cs │ │ │ ├── CertificateBundle.cs │ │ │ └── IssuerBundle.cs │ │ ├── DeletionRecoveryLevel.cs │ │ ├── ValueModel.cs │ │ ├── KeyVaultError.cs │ │ ├── LifetimeActions.cs │ │ ├── TaggedModel.cs │ │ ├── DeletedBundle.cs │ │ └── AttributeBase.cs │ ├── Exceptions │ │ ├── SecretException.cs │ │ ├── MissingItemException.cs │ │ └── ConflictedItemException.cs │ ├── Constants │ │ ├── EnvironmentConstants.cs │ │ ├── SupportedKeyTypes.cs │ │ ├── EncryptionAlgorithms.cs │ │ ├── Orchestration │ │ │ ├── AspireConstants.cs │ │ │ └── WiremockConstants.cs │ │ ├── OperationConstants.cs │ │ ├── AuthConstants.cs │ │ ├── RsaPem.cs │ │ └── CertificateContentType.cs │ ├── Persistence │ │ ├── Interfaces │ │ │ ├── IDeletable.cs │ │ │ ├── IPersistedItem.cs │ │ │ └── INamedItem.cs │ │ ├── SqliteWALCleanupService.cs │ │ ├── Utils │ │ │ ├── CertificateBlobSerializer.cs │ │ │ └── RsaParametersSerializer.cs │ │ └── PersistenceHandler.cs │ ├── Utilities │ │ ├── CacheUtils.cs │ │ ├── Attributes │ │ │ ├── SkipTokenAttribute.cs │ │ │ └── ApiVersionAttribute.cs │ │ ├── EncodingUtils.cs │ │ ├── EnvUtils.cs │ │ ├── PersistenceUtils.cs │ │ ├── TypeExtensions.cs │ │ ├── QueryUtils.cs │ │ └── HttpRequestUtils.cs │ ├── Migrations │ │ └── 20251214064111_ManagedProperty.cs │ ├── Middleware │ │ └── RequestDumpMiddleware.cs │ └── AzureKeyVaultEmulator.Shared.csproj ├── AzureKeyVaultEmulator.Aspire.Hosting │ ├── Exceptions │ │ └── KeyVaultEmulatorException.cs │ ├── Models │ │ └── CertificateLoaderVM.cs │ ├── Constants │ │ ├── KeyVaultEmulatorContainerConstants.cs │ │ └── KeyVaultEmulatorCertConstants.cs │ ├── AzureKeyVaultEmulator.Aspire.Hosting.csproj │ ├── README.md │ └── Helpers │ │ ├── AzureKeyVaultEmulatorClientHelper.cs │ │ └── AzureKeyVaultEnvHelper.cs ├── AzureKeyVaultEmulator │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Keys │ │ ├── Factories │ │ │ └── RsaKeyFactory.cs │ │ ├── Controllers │ │ │ ├── RngController.cs │ │ │ └── DeletedKeysController.cs │ │ └── Services │ │ │ └── IKeyService.cs │ ├── GlobalUsings.cs │ ├── Emulator │ │ ├── Controllers │ │ │ └── EmulatorController.cs │ │ └── Services │ │ │ └── TokenService.cs │ ├── Properties │ │ └── launchSettings.json │ ├── AzureKeyVaultEmulator.csproj │ ├── ApiConfiguration │ │ ├── ServiceRegistration.cs │ │ ├── SwaggerGeneration.cs │ │ └── AuthenticationSetup.cs │ ├── Middleware │ │ ├── ClientRequestIdMiddleware.cs │ │ ├── RestoreDoubleSlashRerouteMiddleware.cs │ │ └── KeyVaultErrorMiddleware.cs │ ├── Secrets │ │ ├── Services │ │ │ └── ISecretService.cs │ │ └── Controllers │ │ │ └── DeletedSecretsController.cs │ ├── Certificates │ │ ├── Services │ │ │ ├── ICertificateBackingService.cs │ │ │ └── ICertificateService.cs │ │ └── Controllers │ │ │ └── DeletedCertificatesController.cs │ └── Program.cs ├── TestContainers │ └── dotnet │ │ ├── Exceptions │ │ └── KeyVaultEmulatorException.cs │ │ ├── Models │ │ ├── CertificateLoaderVM.cs │ │ └── EmulatedTokenCredential.cs │ │ ├── Constants │ │ ├── AzureKeyVaultEmulatorContainerConstants.cs │ │ └── AzureKeyVaultEmulatorCertConstants.cs │ │ ├── AzureKeyVaultEmulator.TestContainers.csproj │ │ └── Helpers │ │ ├── AzureKeyVaultEnvHelper.cs │ │ └── AzureKeyVaultEmulatorClientHelper.cs └── AzureKeyVaultEmulator.Client │ ├── AzureKeyVaultEmulator.Client.csproj │ ├── EmulatedTokenCredential.cs │ ├── KeyVaultHelper.cs │ ├── AddEmulatorSupport.cs │ └── README.md ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── docs-missing.md │ └── bug-report.md └── workflows │ ├── build-and-test.yml │ ├── validate-pull-request.yml │ ├── publish-all-arm-manual.yml │ └── publish-all-manual.yml ├── docs ├── assets │ ├── hero.png │ └── LocalhostPrompt.png ├── CertificateUtilities │ ├── dotnet │ │ ├── create-certs.sh │ │ └── create-certs.ps1 │ ├── openssl │ │ └── create-certs.sh │ └── README.md └── README.md ├── .dockerignore ├── test ├── AzureKeyVaultEmulator.IntegrationTests │ ├── GlobalUsings.cs │ ├── SetupHelper │ │ ├── ClientSetupVM.cs │ │ ├── EmulatedTokenCredential.cs │ │ ├── Fixtures │ │ │ ├── SecretsTestingFixture.cs │ │ │ └── KeysTestingFixture.cs │ │ └── RequestSetup.cs │ ├── Extensions │ │ ├── AzureClientExtensions.cs │ │ └── Assert │ │ │ ├── SharedAsserts.cs │ │ │ └── KeyAsserts.cs │ ├── Certificates │ │ ├── CertificateContactTests.cs │ │ └── Helpers │ │ │ └── X509Certificate With PEM Generation.linq │ ├── AzureKeyVaultEmulator.IntegrationTests.csproj │ └── Keys │ │ └── DeletedKeysControllerTests.cs └── AzureKeyVaultEmulator.TestContainers.Tests │ ├── AzureKeyVaultEmulatorConstantsTests.cs │ └── AzureKeyVaultEmulator.TestContainers.Tests.csproj ├── dev ├── WebApiWithEmulator.DebugHelper │ ├── WebApiPWithEmulator │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── Controllers │ │ │ ├── CertificateController.cs │ │ │ └── SecretsController.cs │ │ ├── WebApiWithEmulator.DebugHelper.csproj │ │ ├── Properties │ │ │ └── launchSettings.json │ │ └── Program.cs │ └── README.md ├── AzureKeyVaultEmulator.AppHost │ └── AzureKeyVaultEmulator.AppHost.AppHost │ │ ├── appsettings.Development.json │ │ ├── Program.cs │ │ ├── appsettings.json │ │ ├── AppHostExtensions.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ └── AzureKeyVaultEmulator.AppHost.csproj └── AzureKeyVaultEmulator.Scripts │ └── migration.ps1 ├── Directory.Build.props ├── SECURITY.md ├── Dockerfile ├── NOTICE.md └── .gitignore /src/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: james-gould 4 | -------------------------------------------------------------------------------- /docs/assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-gould/azure-keyvault-emulator/HEAD/docs/assets/hero.png -------------------------------------------------------------------------------- /docs/assets/LocalhostPrompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-gould/azure-keyvault-emulator/HEAD/docs/assets/LocalhostPrompt.png -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyAttributes.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 2 | 3 | public class KeyAttributes : AttributeBase 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/bin/ 3 | **/obj/ 4 | **/out/ 5 | 6 | # files 7 | Dockerfile* 8 | **/*.trx 9 | **/*.md 10 | **/*.ps1 11 | **/*.cmd 12 | **/*.sh 13 | global.json 14 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/SecretAttributes.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets; 2 | 3 | public class SecretAttributes : AttributeBase 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. 6 | * @james-gould 7 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Exceptions/SecretException.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Exceptions 2 | { 3 | public class SecretException(string msg) : Exception(msg) 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/Exceptions/KeyVaultEmulatorException.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Aspire.Hosting.Exceptions; 2 | 3 | internal class KeyVaultEmulatorException(string msg) : Exception(msg); 4 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Linq; 4 | global using System.Text; 5 | global using System.Threading.Tasks; 6 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Exceptions/MissingItemException.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Exceptions; 2 | 3 | public sealed class MissingItemException(string name) : Exception($"Could not find {name} in vault."); 4 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/EnvironmentConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants; 2 | public sealed class EnvironmentConstants 3 | { 4 | public const string UsePersistedDataStore = "Persist"; 5 | } 6 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.AppHost/AzureKeyVaultEmulator.AppHost.AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/IAttributedModel.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Models; 2 | 3 | public interface IAttributedModel where TAttributes : AttributeBase 4 | { 5 | TAttributes Attributes { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/SupportedKeyTypes.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants 2 | { 3 | public sealed class SupportedKeyTypes 4 | { 5 | public const string RSA = "RSA"; 6 | public const string EC = "EC"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft": "Information", 6 | "Microsoft.EntityFrameworkCore.Database.Command": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/ICreateItem.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Models 2 | { 3 | /// 4 | /// Used to enforce generic constraints between Create models. 5 | /// 6 | public interface ICreateItem 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/ValueResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models 4 | { 5 | public class ValueResponse 6 | { 7 | [JsonPropertyName("value")] 8 | public string Value { get; set; } = string.Empty; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/SetupHelper/ClientSetupVM.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.IntegrationTests.SetupHelper; 2 | 3 | internal sealed class ClientSetupVM(Uri uri, EmulatedTokenCredential cred) 4 | { 5 | internal Uri VaultUri => uri; 6 | internal EmulatedTokenCredential Credential => cred; 7 | } 8 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | true 6 | $(NoWarn);1591 7 | 8 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/Requests/RandomBytesRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 4 | 5 | public sealed class RandomBytesRequest 6 | { 7 | [JsonPropertyName("count")] 8 | public required int Count { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Exceptions/KeyVaultEmulatorException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureKeyVaultEmulator.TestContainers.Exceptions 4 | { 5 | internal class KeyVaultEmulatorException : Exception 6 | { 7 | public KeyVaultEmulatorException(string msg) : base(msg) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.AppHost/AzureKeyVaultEmulator.AppHost.AppHost/Program.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Shared.Constants.Orchestration; 2 | 3 | var builder = DistributedApplication.CreateBuilder(); 4 | 5 | var keyVault = builder.AddProject(AspireConstants.EmulatorServiceName); 6 | 7 | builder.Build().Run(); 8 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This project serves to help debugging the extensions and hosting configuration of the project. 4 | 5 | If you're looking for a sample application using the `AzureKeyVaultEmulator` please head over to [this repository.](https://github.com/james-gould/azure-keyvault-emulator-samples) 6 | 7 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/Interfaces/IDeletable.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 2 | 3 | /// 4 | /// Represents a persisted item that may also be deleted, and potentially restored/purged. 5 | /// 6 | public interface IDeletable 7 | { 8 | bool Deleted { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | }, 8 | "EventLog": { 9 | "LogLevel": { 10 | "Default": "Trace" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "myAspireResourceName": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/CertificateAttributes.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 4 | 5 | public sealed class CertificateAttributes : AttributeBase 6 | { 7 | [JsonPropertyName("version")] 8 | public string Version { get; set; } = string.Empty; 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/Requests/SetContactsRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 4 | 5 | public sealed class SetContactsRequest 6 | { 7 | [JsonPropertyName("contacts")] 8 | public List Contacts { get; set; } = []; 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/EncryptionAlgorithms.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants 2 | { 3 | public sealed class EncryptionAlgorithms 4 | { 5 | public const string RSA1_5 = "RSA1_5"; 6 | public const string RSA_OAEP = "RSA-OAEP"; 7 | public const string RSA_OAEP_256 = "RSA-OAEP-256"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/Interfaces/IPersistedItem.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 2 | 3 | /// 4 | /// Represents an item that is persisted in the database, but always owned by a top-level parent. 5 | /// 6 | public interface IPersistedItem 7 | { 8 | Guid PersistedId { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/DeletedKeyBundle.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 4 | 5 | public sealed class DeletedKeyBundle : DeletedBundle 6 | { 7 | [JsonPropertyName("key")] 8 | public required Microsoft.IdentityModel.Tokens.JsonWebKey Key { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.AppHost/AzureKeyVaultEmulator.AppHost.AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Aspire.Hosting.Dcp": "Warning" 7 | } 8 | }, 9 | "Parameters": { 10 | "Secret": "Value" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please provide as much information as possible to enable us to source and fix any issues you may be encountering. 2 | 3 | ## Expected Behavior 4 | - 5 | 6 | ## Actual Behavior 7 | - 8 | 9 | ## Steps to Reproduce the Problem 10 | 1. 11 | 1. 12 | 1. 13 | 14 | ## Specifications 15 | - Image Version: 16 | - Docker Version: 17 | - .NET Version: 18 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/DeletionRecoveryLevel.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Models 2 | { 3 | public sealed class DeletionRecoveryLevel 4 | { 5 | public const string Purgeable = "Purgeable"; 6 | public const string RecoverablePurgeable = "Recoverable+Purgeable"; 7 | public const string Recoverable = "Recoverable"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/CacheUtils.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Utilities 2 | { 3 | public static class CacheUtils 4 | { 5 | public static string GetCacheId(this string name, string version = "") 6 | { 7 | ArgumentException.ThrowIfNullOrEmpty(name); 8 | 9 | return $"{name}{version}"; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Exceptions/ConflictedItemException.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Exceptions 2 | { 3 | public sealed class ConflictedItemException(string itemType, string name) : Exception($"Conflicted {itemType} {name} found in vault.") 4 | { 5 | public string Name { get; } = name; 6 | 7 | public string ItemType { get; } = itemType; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/CertificateUtilities/dotnet/create-certs.sh: -------------------------------------------------------------------------------- 1 | echo Creating .crt and .pfx to be used in emulator container 2 | 3 | dotnet dev-certs https -ep ./emulator.crt -p emulator -q 4 | 5 | echo Created emulator.crt with password emulator 6 | 7 | dotnet dev-certs https -ep ./emulator.pfx -p emulator -q 8 | 9 | echo Created emulator.pfx with password emulator. You must now install these certificates locally as a Trusted Root CA Authority. -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/Models/CertificateLoaderVM.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | 3 | namespace AzureKeyVaultEmulator.Aspire.Hosting.Models; 4 | 5 | internal sealed class CertificateLoaderVM(string path) 6 | { 7 | public string LocalCertificatePath => path; 8 | public X509Certificate2? Pfx { get; set; } 9 | public string pem { get; set; } = string.Empty; 10 | } 11 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/Requests/SignKeyRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 4 | 5 | public class SignKeyRequest 6 | { 7 | [JsonPropertyName("alg")] 8 | public required string SigningAlgorithm { get; set; } 9 | 10 | [JsonPropertyName("value")] 11 | public required string Value { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/Orchestration/AspireConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants.Orchestration; 2 | 3 | public sealed class AspireConstants 4 | { 5 | public const string EmulatorServiceName = "keyVaultEmulatorApi"; 6 | public const string DebugHelper = "sampleApi"; 7 | public const string Wiremock = "wiremock"; 8 | 9 | public const string IntegrationTest = "integration"; 10 | } 11 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/Interfaces/INamedItem.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 2 | 3 | /// 4 | /// Represents a persisted item that must also be named, allowing for querying through the API. 5 | /// 6 | public interface INamedItem : IPersistedItem, IDeletable 7 | { 8 | string PersistedName { get; set; } 9 | 10 | string PersistedVersion { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /docs/CertificateUtilities/dotnet/create-certs.ps1: -------------------------------------------------------------------------------- 1 | write-host "Creating .crt and .pfx to be used in emulator container" 2 | 3 | dotnet dev-certs https -ep ./emulator.crt -p emulator -q 4 | 5 | write-host "Created emulator.crt with password emulator" 6 | 7 | dotnet dev-certs https -ep ./emulator.pfx -p emulator -q 8 | 9 | write-host "Created emulator.pfx with password emulator. You must now install these certificates locally as a Trusted Root CA Authority." -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyOperationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys 4 | { 5 | public class KeyOperationResult 6 | { 7 | [JsonPropertyName("kid")] 8 | public string KeyIdentifier { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("value")] 11 | public string Data { get; set; } = string.Empty; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyOperationParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys 4 | { 5 | public class KeyOperationParameters 6 | { 7 | [JsonPropertyName("alg")] 8 | public string Algorithm { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("value")] 11 | public string Data { get; set; } = string.Empty; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/Orchestration/WiremockConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants.Orchestration; 2 | 3 | public sealed class WiremockConstants 4 | { 5 | public const string EndpointName = "ensureSsl"; 6 | public const string EndpointPath = $"/{EndpointName}"; 7 | 8 | public const string HttpClientName = "wiremockClient"; 9 | 10 | public const string ConnectivityResponse = "SSL Achieved"; 11 | } 12 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.AppHost/AzureKeyVaultEmulator.AppHost.AppHost/AppHostExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.AppHost; 2 | 3 | internal static class AppHostExtensions 4 | { 5 | public static bool GetFlag(this string[] args, string flagName) 6 | { 7 | var fromArgs = args.FirstOrDefault(x => x.Contains(flagName, StringComparison.InvariantCultureIgnoreCase)); 8 | 9 | return !string.IsNullOrEmpty(fromArgs); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/ValueModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models 4 | { 5 | /// 6 | /// For models that have a "value" property, which can be one of many types. 7 | /// 8 | /// 9 | public class ValueModel 10 | { 11 | [JsonPropertyName("value")] 12 | public required T Value { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/Requests/UpdateCertificateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 4 | 5 | public sealed class UpdateCertificateRequest : TaggedModel 6 | { 7 | [JsonPropertyName("attributes")] 8 | public CertificateAttributes? Attributes { get; set; } 9 | 10 | [JsonPropertyName("policy")] 11 | public CertificatePolicy? Policy { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/ListResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets 4 | { 5 | public class ListResult where TResponse : TaggedModel 6 | { 7 | [JsonPropertyName("nextLink")] 8 | public string NextLink { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("value")] 11 | public IEnumerable Values { get; set; } = []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Azure Key Vault Documentation 2 | 3 | In lieu of a full Wiki the following documentation has been provided: 4 | 5 | - [The setup.sh script, which automates the entire environment configuration process for you.](setup.sh) 6 | - You can use this script without downloading a copy, [more information here.](../README.md#running-the-emulator-with-docker) 7 | - [Configuring the Azure Key Vault Emulator](CONFIG.md) 8 | - [(OUTDATED) Integrating the Emulator into Docker Compose](DOCKER-SETUP.md) -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/DeletedSecretBundle.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets 4 | { 5 | public class DeletedSecretBundle : DeletedBundle 6 | { 7 | 8 | [JsonPropertyName("id")] 9 | public string SecretId { get; set; } = string.Empty; 10 | 11 | [JsonPropertyName("value")] 12 | public string Value { get; set; } = string.Empty; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/Requests/MergeCertificatesRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 4 | 5 | public sealed class MergeCertificatesRequest : TaggedModel 6 | { 7 | [JsonPropertyName("x5c")] 8 | public IEnumerable Certificates { get; set; } = []; 9 | 10 | [JsonPropertyName("attributes")] 11 | public CertificateAttributes Attributes { get; set; } = new(); 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyReleasePolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 4 | 5 | public sealed class KeyReleasePolicy 6 | { 7 | [JsonPropertyName("contentType")] 8 | public string ContentType { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("data")] 11 | public string Data { get; set; } = string.Empty; 12 | 13 | [JsonPropertyName("immutable")] 14 | public bool Immutable { get;set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/Attributes/SkipTokenAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http.Metadata; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | 4 | namespace AzureKeyVaultEmulator.Shared.Utilities.Attributes 5 | { 6 | public sealed class SkipTokenAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromQueryMetadata 7 | { 8 | public string? Name => "$skiptoken"; 9 | 10 | public BindingSource? BindingSource => BindingSource.Query; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/Attributes/ApiVersionAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http.Metadata; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | 4 | namespace AzureKeyVaultEmulator.Shared.Utilities.Attributes 5 | { 6 | public sealed class ApiVersionAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider, IFromQueryMetadata 7 | { 8 | public string? Name => "api-version"; 9 | 10 | public BindingSource? BindingSource => BindingSource.Query; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/Requests/ReleaseKeyRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 4 | 5 | public sealed class ReleaseKeyRequest 6 | { 7 | [JsonPropertyName("target")] 8 | public required string Target { get; set; } 9 | 10 | [JsonPropertyName("enc")] 11 | public string? EncryptionAlgorithm { get; set; } 12 | 13 | [JsonPropertyName("nonce")] 14 | public string? Nonce { get; set; } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/Requests/VerifyHashRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 4 | 5 | public sealed class VerifyHashRequest 6 | { 7 | [JsonPropertyName("alg")] 8 | public required string Algorith { get; set; } 9 | 10 | [JsonPropertyName("digest")] 11 | public required string Digest { get; set; } 12 | 13 | [JsonPropertyName("value")] 14 | public required string Value { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Keys/Factories/RsaKeyFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Factories 4 | { 5 | public static class RsaKeyFactory 6 | { 7 | private const int _defaultSize = 2048; 8 | 9 | public static RSA CreateRsaKey(int? keySize) 10 | { 11 | var adjustedSize = (keySize is not null && keySize != 0) ? keySize : _defaultSize; 12 | 13 | return RSA.Create(adjustedSize ?? _defaultSize); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/KeyVaultError.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models 4 | { 5 | public sealed class KeyVaultError 6 | { 7 | [JsonPropertyName("code")] 8 | public string Code { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("innererror")] 11 | public KeyVaultError? InnerError { get; set; } = null; 12 | 13 | [JsonPropertyName("message")] 14 | public string Message { get; set; } = string.Empty; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Models/CertificateLoaderVM.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | 3 | namespace AzureKeyVaultEmulator.TestContainers.Models 4 | { 5 | internal sealed class CertificateLoaderVM 6 | { 7 | public CertificateLoaderVM(string path) 8 | { 9 | LocalCertificatePath = path; 10 | } 11 | 12 | public string LocalCertificatePath { get; } 13 | public X509Certificate2? Pfx { get; set; } 14 | public string pem { get; set; } = string.Empty; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/CertificateVersionItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 4 | 5 | public sealed class CertificateVersionItem : TaggedModel 6 | { 7 | [JsonPropertyName("id")] 8 | public string Id { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("attributes")] 11 | public CertificateAttributes Attributes { get; set; } = new(); 12 | 13 | [JsonPropertyName("x5t")] 14 | public string Thumbprint { get; set; } = string.Empty; 15 | } 16 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/Requests/CreateCertificateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 4 | 5 | public sealed class CreateCertificateRequest 6 | { 7 | [JsonPropertyName("attributes")] 8 | public CertificateAttributes Attributes { get; set; } = new(); 9 | 10 | [JsonPropertyName("policy")] 11 | public CertificatePolicy CertificatePolicy { get; set; } = new(); 12 | 13 | [JsonPropertyName("tags")] 14 | public Dictionary Tags { get; set; } = []; 15 | } 16 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/Requests/UpdateSecretRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets.Requests 4 | { 5 | public class UpdateSecretRequest 6 | { 7 | [JsonPropertyName("attributes")] 8 | public SecretAttributes? Attributes { get; set; } 9 | 10 | [JsonPropertyName("contentType")] 11 | public string ContentType { get; set; } = string.Empty; 12 | 13 | [JsonPropertyName("tags")] 14 | public Dictionary? Tags { get; set; } = []; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | Please provide a short summary of your changes. If this PR is a draft and should be ignored by maintainers please make sure to mark it as such. 4 | 5 | ## Issue ticket number and link 6 | 7 | * Fixes: #issue-number-here 8 | 9 | ## Checklist before requesting a review 10 | - [ ] I have performed a self-review of my code. 11 | - [ ] I have ran the test suite locally to ensure no breaking changes have been added. 12 | - [ ] I have not removed or changed Azure Key Vault endpoints which break SDK functionality. 13 | - [ ] I have added new tests, if applicable. -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Collections.Concurrent; 2 | global using System.Text; 3 | global using System.Text.Json; 4 | global using AzureKeyVaultEmulator.Emulator.Services; 5 | global using AzureKeyVaultEmulator.Keys.Factories; 6 | global using AzureKeyVaultEmulator.Shared.Constants; 7 | global using AzureKeyVaultEmulator.Shared.Exceptions; 8 | global using AzureKeyVaultEmulator.Shared.Models; 9 | global using AzureKeyVaultEmulator.Shared.Models.Keys; 10 | global using AzureKeyVaultEmulator.Shared.Utilities; 11 | global using AzureKeyVaultEmulator.Shared.Utilities.Attributes; 12 | global using Microsoft.IdentityModel.Tokens; 13 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.Scripts/migration.ps1: -------------------------------------------------------------------------------- 1 | # Azure Key Vault Emulator - Entity Framework Migration Script 2 | # This script contains the commands used to create and manage database migrations 3 | 4 | # Prerequisites: 5 | # 1. Install .NET 9.0 SDK 6 | # 2. Install EF Core tools: dotnet tool install --global dotnet-ef 7 | 8 | # Navigate to the main application project (where DbContext is configured via DI) 9 | Set-Location "src/AzureKeyVaultEmulator" 10 | 11 | # Create a new migration with a dynamically generated name 12 | $migrationName = "Migration_" + [System.Guid]::NewGuid().ToString("N").Substring(0, 8) 13 | dotnet ef migrations add $migrationName --context VaultContext -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/Constants/KeyVaultEmulatorContainerConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Aspire.Hosting; 2 | 3 | internal partial class KeyVaultEmulatorContainerConstants 4 | { 5 | // Image 6 | 7 | public const string Registry = "docker.io"; 8 | public const string Image = "jamesgoulddev/azure-keyvault-emulator"; 9 | public const int Port = 4997; 10 | 11 | public const string Tag = "2.7.7"; 12 | public static string ArmTag => $"{Tag}-arm"; 13 | 14 | } 15 | 16 | internal partial class KeyVaultEmulatorContainerConstants 17 | { 18 | // Environment Variables 19 | 20 | public const string PersistData = "Persist"; 21 | } 22 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/Requests/UpdateKeyRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 4 | 5 | public sealed class UpdateKeyRequest 6 | { 7 | [JsonPropertyName("attributes")] 8 | public KeyAttributes Attributes { get; set; } = new(); 9 | 10 | [JsonPropertyName("tags")] 11 | public Dictionary Tags { get; set; } = []; 12 | 13 | [JsonPropertyName("key_ops")] 14 | public IEnumerable KeyOperations { get; set; } = []; 15 | 16 | [JsonPropertyName("release_policy")] 17 | public KeyReleasePolicy KeyReleasePolicy { get; set; } = new(); 18 | } 19 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Emulator/Controllers/EmulatorController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace AzureKeyVaultEmulator.Emulator.Controllers 4 | { 5 | [Route("")] 6 | public class EmulatorController(ITokenService token) : Controller 7 | { 8 | [HttpGet("token")] 9 | [ProducesResponseType(StatusCodes.Status200OK)] 10 | public IActionResult GenerateStubToken() 11 | { 12 | var jwt = token.CreateBearerToken(); 13 | 14 | return Ok(jwt); 15 | } 16 | 17 | [HttpGet("")] 18 | public IActionResult Root() 19 | { 20 | return Ok(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/LifetimeActions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models; 4 | public sealed class LifetimeActions 5 | { 6 | [JsonPropertyName("trigger")] 7 | public TriggerAction? TriggerAction { get; set; } 8 | 9 | [JsonPropertyName("action")] 10 | public ActionType? Action { get; set; } 11 | } 12 | 13 | public sealed class TriggerAction 14 | { 15 | [JsonPropertyName("timeAfterCreate")] 16 | public string TimeAfterCreate { get; set; } = string.Empty; 17 | } 18 | 19 | public sealed class ActionType 20 | { 21 | [JsonPropertyName("type")] 22 | public string Type { get; set; } = string.Empty; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/Requests/ImportCertificateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 4 | 5 | public sealed class ImportCertificateRequest : ValueModel 6 | { 7 | [JsonPropertyName("attributes")] 8 | public CertificateAttributes Attributes { get; set; } = new(); 9 | 10 | [JsonPropertyName("policy")] 11 | public CertificatePolicy Policy { get; set; } = new(); 12 | 13 | [JsonPropertyName("pwd")] 14 | public string? Password { get; set; } = string.Empty; 15 | 16 | [JsonPropertyName("tags")] 17 | public Dictionary Tags { get; set; } = []; 18 | } 19 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyItemBundle.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 4 | 5 | // https://learn.microsoft.com/en-us/rest/api/keyvault/keys/get-keys/get-keys?view=rest-keyvault-keys-7.4&tabs=HTTP#keyitem 6 | public sealed class KeyItemBundle : TaggedModel 7 | { 8 | [JsonPropertyName("attributes")] 9 | public KeyAttributes KeyAttributes { get; set; } = new(); 10 | 11 | [JsonPropertyName("kid")] 12 | public required string KeyId { get; set; } 13 | 14 | [JsonPropertyName("managed")] 15 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 16 | public bool? Managed { get; set; } = null; 17 | } 18 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/SecretProperties.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Text.Json.Serialization; 4 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 5 | 6 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets; 7 | 8 | public sealed class SecretProperties : IPersistedItem 9 | { 10 | [Key] 11 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 12 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 13 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 14 | 15 | [JsonPropertyName("contentType")] 16 | public string ContentType { get; set; } = string.Empty; 17 | } 18 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/OperationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants; 2 | 3 | public sealed class OperationConstants 4 | { 5 | // https://github.com/Azure/azure-sdk-for-net/blob/48f989c17416fd9a5620c74a45823bd3668b88c1/sdk/keyvault/Azure.Security.KeyVault.Certificates/src/CertificateOperation.cs#L16-L17 6 | // "pending" denoted in CertificateClient docs, but not present in SDK client 7 | public const string Pending = "pending"; 8 | public const string Completed = "completed"; 9 | public const string Cancelled = "cancelled"; 10 | 11 | public const string CompletedReason = $"All operations are immediately completed in the {AuthConstants.EmulatorName}"; 12 | } 13 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/Requests/SetSecretRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets.Requests 4 | { 5 | public class SetSecretRequest : ICreateItem 6 | { 7 | [JsonPropertyName("value")] 8 | public required string Value { get; set; } 9 | 10 | [JsonPropertyName("contentType")] 11 | public string ContentType { get; set; } = string.Empty; 12 | 13 | [JsonPropertyName("attributes")] 14 | public SecretAttributes SecretAttributes { get; set; } = new(); 15 | 16 | [JsonPropertyName("tags")] 17 | public Dictionary Tags { get; set; } = []; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Constants/AzureKeyVaultEmulatorContainerConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.TestContainers.Constants 2 | { 3 | internal partial class AzureKeyVaultEmulatorContainerConstants 4 | { 5 | // Image 6 | 7 | public const string Registry = "docker.io"; 8 | public const string Image = "jamesgoulddev/azure-keyvault-emulator"; 9 | public const int Port = 4997; 10 | 11 | public const string Tag = "2.7.7"; 12 | public static string ArmTag => $"{Tag}-arm"; 13 | } 14 | 15 | internal partial class AzureKeyVaultEmulatorContainerConstants 16 | { 17 | // Environment Variables 18 | 19 | public const string PersistData = "Persist"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Keys/Controllers/RngController.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Keys.Services; 2 | using AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace AzureKeyVaultEmulator.Keys.Controllers; 7 | 8 | [ApiController] 9 | [Route("")] 10 | [Authorize] 11 | public class RngController(IKeyService keyService) : Controller 12 | { 13 | [HttpPost("rng")] 14 | public IActionResult GetRandomBytes( 15 | [FromBody] RandomBytesRequest request, 16 | [ApiVersion] string apiVersion 17 | ) 18 | { 19 | var result = keyService.GetRandomBytes(request.Count); 20 | 21 | return Ok(result); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/Requests/ImportKeyRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Keys.RequestModels; 4 | 5 | public sealed class ImportKeyRequest 6 | { 7 | [JsonPropertyName("key")] 8 | public required Microsoft.IdentityModel.Tokens.JsonWebKey Key { get; set; } 9 | 10 | [JsonPropertyName("hsm")] 11 | public bool? HSM { get; set; } 12 | 13 | [JsonPropertyName("attributes")] 14 | public KeyAttributes KeyAttributes { get; set; } = new(); 15 | 16 | [JsonPropertyName("release_policy")] 17 | public object? ReleasePolicy { get; set; } 18 | 19 | [JsonPropertyName("tags")] 20 | public Dictionary Tags { get; set; } = []; 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature you think would be a good addition to the Emulator. 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/SqliteWALCleanupService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Persistence; 6 | public class SqliteWALCleanupService(IServiceProvider services) : IHostedService 7 | { 8 | public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; 9 | 10 | public async Task StopAsync(CancellationToken token) 11 | { 12 | using var scope = services.CreateScope(); 13 | var db = scope.ServiceProvider.GetRequiredService(); 14 | 15 | await db.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(FULL);", token); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/Utils/CertificateBlobSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | 3 | public static class CertificateBlobSerializer 4 | { 5 | public static byte[] Serialize(X509Certificate2 cert, string? password = null) 6 | { 7 | return cert.Export( 8 | X509ContentType.Pkcs12, // preserves private key for PFX 9 | password ?? string.Empty 10 | ); 11 | } 12 | 13 | public static X509Certificate2 Deserialize(byte[] blob, string? password = null) 14 | { 15 | return X509CertificateLoader.LoadPkcs12( 16 | blob, 17 | password, 18 | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/SecretItemBundle.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets; 4 | 5 | public class SecretItemBundle : TaggedModel 6 | { 7 | [JsonPropertyName("attributes")] 8 | public SecretAttributes SecretAttributes { get; set; } = new(); 9 | 10 | [JsonPropertyName("id")] 11 | public string Id { get; set; } = string.Empty; 12 | 13 | [JsonPropertyName("contentType")] 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | public string? ContentType { get; set; } = null; 16 | 17 | [JsonPropertyName("managed")] 18 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 19 | public bool? Managed { get; set; } = null; 20 | } 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.2.0 | :white_check_mark: | 8 | | 2.1.0 | :x: | 9 | | 1.0.6 | :x: | 10 | | < 1.0.6 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please raise an issue if you discover a vulnerability, optionally including the OWASP identifier, and a description of the impact. 15 | 16 | If you wish to privately disclose the vulnerability you can also [email it privately here](hello@jamesgould.dev). 17 | 18 | ## Usage 19 | 20 | Please be aware that the emulator is **not** a suitable place to store production secrets, and should ideally be used with `ContainerLifetime.Session` to purge all persisted secrets on destroy. 21 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.AppHost/AzureKeyVaultEmulator.AppHost.AppHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "AzureKeyVaultEmulator Host": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:17138;http://localhost:15023", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21131", 13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22102", 14 | "Override": "false", 15 | "Persisted": "false" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/TaggedModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Models 6 | { 7 | public class TaggedModel 8 | { 9 | [NotMapped] 10 | [JsonPropertyName("tags")] 11 | public Dictionary Tags { get; set; } = []; 12 | 13 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 14 | [Column("Tags")] 15 | public string TagsSerialized 16 | { 17 | get => JsonSerializer.Serialize(Tags); 18 | set => Tags = string.IsNullOrEmpty(value) 19 | ? [] 20 | : JsonSerializer.Deserialize>(value) ?? []; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/Extensions/AzureClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Certificates; 2 | 3 | namespace AzureKeyVaultEmulator.IntegrationTests.Extensions; 4 | 5 | internal static class AzureClientExtensions 6 | { 7 | /// 8 | /// Unwraps the structure from the client to keep tests a bit cleaner. 9 | /// 10 | internal static async Task GetCertAsync( 11 | this CertificateClient client, 12 | string certName, 13 | CancellationToken cancellationToken = default) 14 | { 15 | var response = await client.GetCertificateAsync(certName, cancellationToken); 16 | 17 | return response.Value; 18 | } 19 | 20 | // Insert other client extensions below and refactor 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build 2 | WORKDIR /app 3 | 4 | COPY AzureKeyVaultEmulator.sln ./ 5 | COPY src/AzureKeyVaultEmulator/*.csproj src/AzureKeyVaultEmulator/ 6 | COPY src/AzureKeyVaultEmulator.Shared/*.csproj src/AzureKeyVaultEmulator.Shared/ 7 | RUN dotnet restore src/AzureKeyVaultEmulator/AzureKeyVaultEmulator.csproj 8 | 9 | COPY . ./ 10 | WORKDIR /app/src/AzureKeyVaultEmulator 11 | RUN dotnet publish -c Release -o /out 12 | 13 | FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime 14 | WORKDIR /app 15 | 16 | ENV ASPNETCORE_URLS=https://+:4997 17 | ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/emulator.pfx 18 | ENV ASPNETCORE_Kestrel__Certificates__Default__Password=emulator 19 | 20 | COPY --from=build /out ./ 21 | 22 | EXPOSE 4997 23 | 24 | ENTRYPOINT ["dotnet", "AzureKeyVaultEmulator.dll"] 25 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/EncodingUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Utilities 4 | { 5 | public static class EncodingUtils 6 | { 7 | public static string Base64UrlEncode(this byte[]? bytes) 8 | { 9 | ArgumentNullException.ThrowIfNull(bytes); 10 | 11 | return new StringBuilder(Convert.ToBase64String(bytes)).Replace('+', '-').Replace('/', '_').Replace("=", "").ToString(); 12 | } 13 | 14 | public static byte[] Base64UrlDecode(this string encoded) 15 | { 16 | encoded = new StringBuilder(encoded).Replace('-', '+').Replace('_', '/').Append('=', (encoded.Length % 4 == 0) ? 0 : 4 - (encoded.Length % 4)).ToString(); 17 | 18 | return Convert.FromBase64String(encoded); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs-missing.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation missing/outdated 3 | about: Suggest a fix or extensions of the documentation that may be incorrect/missing. 4 | title: "[Docs]" 5 | labels: documentation 6 | assignees: 'james-gould' 7 | 8 | --- 9 | 10 | **How are you using the Emulator?** 11 | 12 | - [ ] .NET Aspire 13 | - [ ] Docker 14 | - [ ] Something else (elaborate below) 15 | 16 | **Is this about missing or outdated documentation?** 17 | 18 | - [ ] Missing 19 | - [ ] Outdated 20 | 21 | **Which documentation requires correcting? (please link it!)** 22 | 23 | - Add your link(s) here to the docs in the repository (ending in `.md`) 24 | 25 | **Describe the issue** 26 | A clear and concise description of what is wrong with the documentation. 27 | 28 | **Additional context** 29 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/PersistenceHandler.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Shared.Constants; 2 | using AzureKeyVaultEmulator.Shared.Persistence; 3 | using AzureKeyVaultEmulator.Shared.Utilities; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace AzureKeyVaultEmulator.ApiConfiguration; 8 | 9 | public static class PersistenceHandler 10 | { 11 | public static IServiceCollection AddVaultPersistenceLayer(this IServiceCollection services) 12 | { 13 | var shouldPersist = EnvironmentConstants.UsePersistedDataStore.GetFlag(); 14 | 15 | var connectionString = PersistenceUtils.CreateSQLiteConnectionString(shouldPersist); 16 | 17 | services.AddDbContext(options => options.UseSqlite(connectionString)); 18 | 19 | return services; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "AzureKeyVaultEmulator": { 5 | "commandName": "Project", 6 | "launchUrl": "swagger", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "dotnetRunMessages": true, 11 | "applicationUrl": "https://localhost:4997" 12 | }, 13 | "Container (Dockerfile)": { 14 | "commandName": "Docker", 15 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_HTTPS_PORTS": "4997" 18 | }, 19 | "publishAllPorts": true, 20 | "useSSL": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/AuthConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.IdentityModel.Tokens; 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Constants 6 | { 7 | public static class AuthConstants 8 | { 9 | private const string _issuerSigningKey = "VZboShdn5FpO2b2iHA7pzhJpmc24e8u9"; 10 | 11 | public static Regex JwtRegex = new Regex("(^[\\w-]*\\.[\\w-]*\\.[\\w-]*$)", RegexOptions.Compiled); 12 | 13 | public const string EmulatorName = "Azure Key Vault Emulator"; 14 | 15 | public const string EmulatorUri = "https://azure-keyvault-emulator.vault.azure.net"; 16 | 17 | public const string EmulatorIss = "localazurekeyvault.localhost.com"; 18 | 19 | public static readonly SymmetricSecurityKey SigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_issuerSigningKey)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/EnvUtils.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Utilities; 2 | 3 | public static class EnvUtils 4 | { 5 | public static string GetEnvVarOrDefault(this string envVar, string defaultValue) 6 | => Environment.GetEnvironmentVariable(envVar) ?? defaultValue; 7 | 8 | public static bool GetFlag(this string flagName) 9 | { 10 | var fromEnv = Environment.GetEnvironmentVariable(flagName); 11 | 12 | return !string.IsNullOrEmpty(fromEnv) && ConvertTo(fromEnv); 13 | } 14 | 15 | private static T ConvertTo(string value) 16 | { 17 | try 18 | { 19 | return (T)Convert.ChangeType(value, typeof(T)); 20 | } 21 | catch (InvalidCastException) 22 | { 23 | throw new InvalidCastException($"Cannot convert {value} to {typeof(T).Name}"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > As of `10/29/2024` [Basis Theory](https://github.com/Basis-Theory/azure-keyvault-emulator) is no longer maintaining the base repository, although the development seemed to be ceased for quite some time prior to that. Due to the ever-growing popularity of `.NET Aspire` the emulator is becoming increasingly more important; I've forked the repository with the goal to create a feature-complete, `.NET Aspire` supported emulator.

3 | > This repo has been detached from the base repo as of `22/03/2025` due to the change in direction and aspirations for the project., but I cannot thank Basis Theory enough for the original codebase. 4 | > The original license holder retains their copyright to all original source code, additions or changes beyond [this commit](https://github.com/james-gould/azure-keyvault-emulator/commit/5a9bbb94ca7f52755144fd20d7966575530e0275) are now held by James Gould, the author. 5 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/AzureKeyVaultEmulator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net10.0 4 | enable 5 | true 6 | 7 | Linux 8 | enable 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Constants/AzureKeyVaultEmulatorCertConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.TestContainers.Constants 2 | { 3 | internal sealed class AzureKeyVaultEmulatorCertConstants 4 | { 5 | private const string _rootName = "emulator"; 6 | 7 | public const string HostParentDirectory = "keyvaultemulator"; 8 | public const string HostChildDirectory = "certs"; 9 | 10 | // PFX is referenced in the Dockerfile, update both is this changes. 11 | public const string Pfx = "emulator.pfx"; 12 | public const string Crt = "emulator.crt"; 13 | 14 | // This is also referenced in the Dockerfile, update both if this changes. 15 | public const string Pword = _rootName; 16 | 17 | public const string CertMountTarget = "/certs"; 18 | 19 | public const string Subject = "CN=localhost"; 20 | 21 | public const string LinuxPath = "/usr/local/share/ca-certificates"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/PersistenceUtils.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Utilities; 2 | 3 | public static class PersistenceUtils 4 | { 5 | /// 6 | /// Constructs the ConnectionString for an SQLite database. 7 | /// 8 | /// Flag for using in memory or persisted on disk. 9 | /// A fully qualified connection string. 10 | public static string CreateSQLiteConnectionString(bool shouldPersist) 11 | { 12 | var root = "Data Source="; 13 | var dbName = shouldPersist ? "emulator" : Guid.NewGuid().Neat(); 14 | var dbDir = shouldPersist ? "/certs/" : Path.GetTempPath(); 15 | 16 | #if DEBUG 17 | if (shouldPersist) 18 | dbDir = $"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}/keyvaultemulator/certs/"; 19 | #endif 20 | 21 | return $"{root}{dbDir}{dbName}.db"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Raise an issue regarding a bug in the Emulator. 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **I'm using** 11 | - [ ] .NET Aspire 12 | - [ ] Docker without Aspire 13 | - [ ] TestContainers 14 | - [ ] Something else (please elaborate below) 15 | 16 | **This happened using...** 17 | 18 | - [ ] An official Azure SDK client 19 | - [ ] The REST API 20 | - [ ] Something else (please elaborate below) 21 | 22 | **Describe the bug** 23 | A clear and concise description of what the bug is. 24 | 25 | **To Reproduce** 26 | Include either steps to reproduce, or the action you attempted which didn't work as expected. 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Operating system** 32 | Provide your operating system and version. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/Controllers/CertificateController.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Certificates; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace WebApiWithEmulator.DebugHelper.Controllers; 5 | public class CertificateController(CertificateClient client) : Controller 6 | { 7 | [HttpPatch("updateCertificate")] 8 | public async Task UpdateCertificate() 9 | { 10 | var name = Guid.NewGuid().ToString(); 11 | 12 | var operation = await client.StartCreateCertificateAsync(name, CertificatePolicy.Default); 13 | 14 | await operation.WaitForCompletionAsync(); 15 | 16 | var cert = await client.GetCertificateAsync(name); 17 | 18 | var props = new CertificateProperties(name) 19 | { 20 | Enabled = false, 21 | }; 22 | 23 | var response = await client.UpdateCertificatePropertiesAsync(props); 24 | 25 | return Ok(response.Value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/CertificateUtilities/openssl/create-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo Creating certificates using openssl 3 | 4 | CONFIG=$(cat < 6 | /// Used to create a bearer token with the emulated /token endpoint, which then passes the auth checks internally 7 | /// 8 | /// 9 | internal sealed class EmulatedTokenCredential(string token) : TokenCredential 10 | { 11 | private string _token = token; 12 | 13 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 14 | { 15 | return new AccessToken(_token, DateTimeOffset.Now.AddDays(30)); 16 | } 17 | 18 | public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 19 | { 20 | return new ValueTask(GetToken(requestContext, cancellationToken)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/DeletedBundle.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models; 4 | 5 | public class DeletedBundle : TaggedModel 6 | where TAttributes : AttributeBase 7 | { 8 | [JsonPropertyName("attributes")] 9 | public TAttributes? Attributes { get; set; } 10 | 11 | [JsonPropertyName("contentType")] 12 | public string ContentType { get; set; } = string.Empty; 13 | 14 | [JsonPropertyName("kid")] 15 | public string Kid { get; set; } = string.Empty; 16 | 17 | [JsonPropertyName("managed")] 18 | public bool Managed { get; set; } 19 | 20 | [JsonPropertyName("recoveryId")] 21 | public required string RecoveryId { get; set; } 22 | 23 | [JsonPropertyName("deletedDate")] 24 | public long DeletedDate { get; set; } = DateTimeOffset.Now.ToUnixTimeSeconds(); 25 | 26 | [JsonPropertyName("scheduledPurgeDate")] 27 | public long ScheduledPurgeDateUTC { get; set; } = DateTimeOffset.Now.ToUnixTimeSeconds(); 28 | } 29 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/Constants/KeyVaultEmulatorCertConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Aspire.Hosting.Constants; 2 | 3 | public sealed class KeyVaultEmulatorCertConstants 4 | { 5 | private const string _rootName = "emulator"; 6 | 7 | public const string HostParentDirectory = "keyvaultemulator"; 8 | public const string HostChildDirectory = "certs"; 9 | public const string HostDotnetDevCertsChildDirectory = "dotnet-dev-certs"; 10 | 11 | // PFX is referenced in the Dockerfile, update both is this changes. 12 | public const string Pfx = $"{_rootName}.pfx"; 13 | public const string Crt = $"{_rootName}.crt"; 14 | public const string Pem = $"{_rootName}.pem"; 15 | 16 | // This is also referenced in the Dockerfile, update both if this changes. 17 | public const string Pword = _rootName; 18 | 19 | public const string CertMountTarget = "/certs"; 20 | 21 | public const string Subject = "CN=localhost"; 22 | 23 | public const string LinuxPath = "/usr/local/share/ca-certificates"; 24 | } 25 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/Extensions/Assert/SharedAsserts.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | 3 | namespace Xunit; 4 | 5 | public partial class Assert 6 | { 7 | /// 8 | /// Attempts expecting a and Asserts ResponseCode == HttpStatusCode.BadRequest 9 | /// 10 | /// The response object for 11 | /// The client func to execute expecting a failure. 12 | /// Denotes the expected status code for the request, typically NotFound. 13 | public static async Task RequestFailsAsync( 14 | Func> clientAction, 15 | HttpStatusCode expectedStatusCode = HttpStatusCode.NotFound) 16 | where TResult : class 17 | { 18 | var exception = await ThrowsAsync(clientAction); 19 | 20 | Equal((int)expectedStatusCode, exception?.Status); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/CreateKey.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace AzureKeyVaultEmulator.Shared.Models.Keys 5 | { 6 | public class CreateKey : ICreateItem 7 | { 8 | [JsonPropertyName("kty")] 9 | [Required] 10 | public string KeyType { get; set; } = string.Empty; 11 | 12 | [JsonPropertyName("attributes")] 13 | public KeyAttributes KeyAttributes { get; set; } = new(); 14 | 15 | [JsonPropertyName("release_policy")] 16 | public KeyReleasePolicy? keyReleasePolicy { get; set; } 17 | 18 | [JsonPropertyName("crv")] 19 | public string KeyCurveName { get; set; } = string.Empty; 20 | 21 | [JsonPropertyName("key_ops")] 22 | public List KeyOperations { get; set; } = []; 23 | 24 | [JsonPropertyName("key_size")] 25 | public int KeySize { get; set; } = 2048; 26 | 27 | [JsonPropertyName("tags")] 28 | public Dictionary? Tags { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyProperties.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Text.Json.Serialization; 4 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 5 | 6 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 7 | 8 | public sealed class KeyProperties : IPersistedItem 9 | { 10 | [Key] 11 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 12 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 13 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 14 | 15 | [JsonPropertyName("crv")] 16 | public string JsonWebKeyCurveName { get; set; } = string.Empty; 17 | 18 | [JsonPropertyName("exportable")] 19 | public static bool Exportable => true; 20 | 21 | [JsonPropertyName("key_size")] 22 | public int KeySize { get; set; } = 2048; 23 | 24 | [JsonPropertyName("kty")] 25 | public string JsonWebKeyType { get; set; } = string.Empty; 26 | 27 | [JsonPropertyName("reuse_key")] 28 | public bool ReuseKey { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.TestContainers.Tests/AzureKeyVaultEmulatorConstantsTests.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.TestContainers.Constants; 2 | using Xunit; 3 | 4 | namespace AzureKeyVaultEmulator.TestContainers.Tests; 5 | 6 | /// 7 | /// Tests for the AzureKeyVaultEmulatorConstants class. 8 | /// 9 | public class AzureKeyVaultEmulatorConstantsTests 10 | { 11 | [Fact] 12 | public void Constants_HaveExpectedValues() 13 | { 14 | // Assert 15 | Assert.Equal("docker.io", AzureKeyVaultEmulatorContainerConstants.Registry); 16 | Assert.Equal("jamesgoulddev/azure-keyvault-emulator", AzureKeyVaultEmulatorContainerConstants.Image); 17 | Assert.NotEqual("latest", AzureKeyVaultEmulatorContainerConstants.Tag); 18 | Assert.Equal(4997, AzureKeyVaultEmulatorContainerConstants.Port); 19 | Assert.Equal("/certs", AzureKeyVaultEmulatorCertConstants.CertMountTarget); 20 | Assert.Equal("emulator.pfx", AzureKeyVaultEmulatorCertConstants.Pfx); 21 | Assert.Equal("Persist", AzureKeyVaultEmulatorContainerConstants.PersistData); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/ApiConfiguration/ServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Certificates.Services; 2 | using AzureKeyVaultEmulator.Keys.Services; 3 | using AzureKeyVaultEmulator.Secrets.Services; 4 | using AzureKeyVaultEmulator.Shared.Persistence; 5 | 6 | namespace AzureKeyVaultEmulator.ApiConfiguration 7 | { 8 | public static class ServiceRegistration 9 | { 10 | public static IServiceCollection RegisterCustomServices(this IServiceCollection services) 11 | { 12 | services.AddTransient(); 13 | services.AddTransient(); 14 | 15 | services.AddTransient(); 16 | services.AddTransient(); 17 | 18 | services.AddTransient(); 19 | services.AddTransient(); 20 | 21 | services.AddHostedService(); 22 | 23 | return services; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/KeyVaultContact.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 4 | 5 | /// 6 | /// Although technically for certificates, the actual use case of Contacts is at a key vault level. 7 | /// They're only referenced in the Certificates API documentation, ideally this would be a shared class but keeping it domain specific follows the existing pattern. 8 | /// Note: this is a different entity to , although they're very similar. 9 | /// Reference documentation: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/set-certificate-contacts/set-certificate-contacts 10 | /// 11 | public sealed class KeyVaultContact 12 | { 13 | [JsonPropertyName("email")] 14 | public string Email { get; set; } = string.Empty; 15 | 16 | [JsonPropertyName("name")] 17 | public string Name { get; set; } = string.Empty; 18 | 19 | [JsonPropertyName("phone")] 20 | public string Phone { get; set; } = string.Empty; 21 | } 22 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/WebApiWithEmulator.DebugHelper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-test: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: | 20 | 8.0.x 21 | 9.0.x 22 | 10.0.x 23 | 24 | - name: Install SSL Certificates 25 | run: | 26 | dotnet dev-certs https --clean 27 | dotnet dev-certs https --trust 28 | 29 | - name: Restore dependencies 30 | run: dotnet restore 31 | 32 | - name: Dotnet Build 33 | run: dotnet build --configuration Release --no-restore 34 | 35 | - name: Run API Integration Tests 36 | run: dotnet test ./test/AzureKeyVaultEmulator.IntegrationTests/AzureKeyVaultEmulator.IntegrationTests.csproj 37 | 38 | - name: Run TestContainers Integration Tests 39 | run: dotnet test ./test/AzureKeyVaultEmulator.TestContainers.Tests/AzureKeyVaultEmulator.TestContainers.Tests.csproj 40 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Middleware/ClientRequestIdMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Middleware 2 | { 3 | public class ClientRequestIdMiddleware 4 | { 5 | private readonly RequestDelegate _next; 6 | 7 | public ClientRequestIdMiddleware(RequestDelegate next) 8 | { 9 | _next = next; 10 | } 11 | 12 | public async Task InvokeAsync(HttpContext context) 13 | { 14 | var clientRequestId = context.Request.Headers["x-ms-client-request-id"].FirstOrDefault(); 15 | var returnHeaderFlag = context.Request.Headers["x-ms-return-client-request-id"].FirstOrDefault(); 16 | 17 | if (!string.IsNullOrEmpty(clientRequestId) && !string.IsNullOrEmpty(returnHeaderFlag) && 18 | returnHeaderFlag.Equals("true", StringComparison.OrdinalIgnoreCase)) 19 | { 20 | context.Response.OnStarting(() => 21 | { 22 | context.Response.Headers["x-ms-client-request-id"] = clientRequestId; 23 | return Task.CompletedTask; 24 | }); 25 | } 26 | 27 | await _next(context); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyRotationPolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using AzureKeyVaultEmulator.Shared.Constants; 3 | 4 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 5 | 6 | public sealed class KeyRotationPolicy 7 | { 8 | [JsonPropertyName("id")] 9 | public string Id { get; set; } = string.Empty; 10 | 11 | [JsonPropertyName("attributes")] 12 | public KeyRotationAttributes Attributes { get; set; } = new(); 13 | 14 | public IEnumerable LifetimeActions { get; set; } = []; 15 | 16 | public void SetIdFromKeyName(string keyName) 17 | => Id = $"{AuthConstants.EmulatorUri}/keys/{keyName}/rotationpolicy"; 18 | } 19 | 20 | public class KeyRotationAttributes 21 | { 22 | [JsonPropertyName("expiryTime")] 23 | public string ExpiryTime { get; set; } = string.Empty; 24 | 25 | [JsonPropertyName("created")] 26 | public long CreatedTsUnix { get; set; } = DateTimeOffset.Now.ToUnixTimeSeconds(); 27 | 28 | [JsonPropertyName("updated")] 29 | public long UpdatedTsUnix { get; set; } = DateTimeOffset.Now.ToUnixTimeSeconds(); 30 | 31 | public void Update() => UpdatedTsUnix = DateTimeOffset.Now.ToUnixTimeSeconds(); 32 | } 33 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyReleaseVM.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using AzureKeyVaultEmulator.Shared.Constants; 3 | 4 | namespace AzureKeyVaultEmulator.Shared.Models.Keys; 5 | 6 | public sealed class KeyReleaseVM(string aas) 7 | { 8 | [JsonPropertyName("aas-ehd")] 9 | public string AasEhd { get; set; } = aas; 10 | 11 | [JsonPropertyName("iss")] 12 | public static string Issuer => $"{AuthConstants.EmulatorUri}/keys"; 13 | 14 | [JsonPropertyName("sgx-mrenclave")] 15 | public static string SGXEnclave => "0000000000000000000000000000000000000000000000000000000000000000"; 16 | 17 | [JsonPropertyName("sgx-mrsigner")] 18 | public static string SGXMrsigner => "86788fe40448f2a12e20bf8d5e7a1c3139bc5fdc1432b370c1da3489ab649a85"; 19 | 20 | [JsonPropertyName("is-debuggable")] 21 | public static bool Debuggable => true; 22 | 23 | [JsonPropertyName("tee")] 24 | public static string Tee => "sgx"; 25 | 26 | [JsonPropertyName("iat")] 27 | public static long IssuedAt => DateTimeOffset.Now.ToUnixTimeSeconds(); 28 | 29 | [JsonPropertyName("exp")] 30 | public static long Expiry => DateTimeOffset.Now.AddDays(31).ToUnixTimeSeconds(); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/Controllers/SecretsController.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Secrets; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace WebApiWithEmulator.Controllers 5 | { 6 | [Route("secrets")] 7 | public class SecretsController : ControllerBase 8 | { 9 | private SecretClient _secretClient; 10 | 11 | public SecretsController(SecretClient secretClient) 12 | { 13 | _secretClient = secretClient; 14 | } 15 | 16 | [HttpGet("create")] 17 | public async Task CreateSecret( 18 | [FromQuery] string name = "test", 19 | [FromQuery] string value = "123") 20 | { 21 | var secret = await _secretClient.SetSecretAsync(name, value); 22 | 23 | return Ok(secret); 24 | } 25 | 26 | [HttpGet("get")] 27 | public async Task GetSecret([FromQuery] string name = "test") 28 | { 29 | var secretFromContainer = await _secretClient.GetSecretAsync(name); 30 | 31 | return secretFromContainer is null ? NotFound($"Could not find secret with name {name}") : Ok(secretFromContainer); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Migrations/20251214064111_ManagedProperty.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Migrations 6 | { 7 | /// 8 | public partial class ManagedProperty : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Managed", 15 | table: "Secrets", 16 | type: "INTEGER", 17 | nullable: true); 18 | 19 | migrationBuilder.AddColumn( 20 | name: "Managed", 21 | table: "Keys", 22 | type: "INTEGER", 23 | nullable: true); 24 | } 25 | 26 | /// 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.DropColumn( 30 | name: "Managed", 31 | table: "Secrets"); 32 | 33 | migrationBuilder.DropColumn( 34 | name: "Managed", 35 | table: "Keys"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Keys/KeyBundle.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json.Serialization; 4 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 5 | 6 | namespace AzureKeyVaultEmulator.Shared.Models.Keys 7 | { 8 | public class KeyBundle : TaggedModel, INamedItem, IAttributedModel 9 | { 10 | [Key] 11 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 12 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 13 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 14 | 15 | public string PersistedName { get; set; } = string.Empty; 16 | 17 | public string PersistedVersion { get; set; } = string.Empty; 18 | 19 | [JsonPropertyName("key")] 20 | public required InternalJsonWebKey Key { get; set; } 21 | 22 | [JsonPropertyName("managed")] 23 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 24 | public bool? Managed { get; set; } = null; 25 | 26 | [JsonPropertyName("attributes")] 27 | public KeyAttributes Attributes { get; set; } = new(); 28 | 29 | [JsonIgnore] 30 | public bool Deleted { get; set; } = false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/validate-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Validate Pull Request 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | - edited 9 | jobs: 10 | run-integration-tests: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout PR branch (from fork or same repo) 15 | uses: actions/checkout@v4 16 | with: 17 | repository: ${{ github.event.pull_request.head.repo.full_name }} 18 | ref: ${{ github.event.pull_request.head.ref }} 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | dotnet-version: | 25 | 8.0.x 26 | 9.0.x 27 | 10.0.x 28 | 29 | - name: Install SSL Certificates 30 | run: | 31 | dotnet dev-certs https --clean 32 | dotnet dev-certs https --trust 33 | 34 | - name: Run API Integration Tests 35 | run: dotnet test ./test/AzureKeyVaultEmulator.IntegrationTests/AzureKeyVaultEmulator.IntegrationTests.csproj 36 | 37 | - name: Run TestContainers Integration Tests 38 | run: dotnet test ./test/AzureKeyVaultEmulator.TestContainers.Tests/AzureKeyVaultEmulator.TestContainers.Tests.csproj 39 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Utilities; 2 | 3 | public static class TypeExtensions 4 | { 5 | /// 6 | /// Converts a to a and returns the epoch time. 7 | /// 8 | /// The to convert. 9 | /// Optional timezone to calculate the UTC offset from. 10 | /// The epoch time, offset by . 11 | public static long ToUnixTimeSeconds(this DateTime dt, string timeZone = "") 12 | { 13 | if (string.IsNullOrEmpty(timeZone)) 14 | return new DateTimeOffset(dt).ToUnixTimeSeconds(); 15 | 16 | var dto = new DateTimeOffset(dt, TimeZoneInfo.FindSystemTimeZoneById(timeZone).GetUtcOffset(dt)); 17 | 18 | return dto.ToUnixTimeSeconds(); 19 | } 20 | 21 | /// 22 | /// Formats the in lowercase with no hyphens ("-"). 23 | /// 24 | /// The guid to format. 25 | /// A formatted 26 | public static string Neat(this Guid guid) 27 | { 28 | return guid.ToString("n"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5195", 8 | "sslPort": 44344 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5234", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7211;http://localhost:5234", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Secrets/Services/ISecretService.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Shared.Models.Secrets; 2 | using AzureKeyVaultEmulator.Shared.Models.Secrets.Requests; 3 | 4 | namespace AzureKeyVaultEmulator.Secrets.Services 5 | { 6 | public interface ISecretService 7 | { 8 | Task GetSecretAsync(string name, string version = ""); 9 | Task SetSecretAsync(string name, SetSecretRequest requestBody, bool? managed = null); 10 | Task DeleteSecretAsync(string name, string version = ""); 11 | Task> BackupSecretAsync(string name); 12 | Task GetDeletedSecretAsync(string name); 13 | ListResult GetDeletedSecrets(int maxVersions = 25, int skipCount = 0); 14 | ListResult GetSecretVersions(string secretName, int maxResults = 25, int skipCount = 0); 15 | ListResult GetSecrets(int maxResults = 25, int skipCount = 0); 16 | Task PurgeDeletedSecretAsync(string name); 17 | Task RecoverDeletedSecretAsync(string name); 18 | Task RestoreSecretAsync(string encodedName); 19 | Task UpdateSecretAsync(string name, string version, UpdateSecretRequest request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dev/AzureKeyVaultEmulator.AppHost/AzureKeyVaultEmulator.AppHost.AppHost/AzureKeyVaultEmulator.AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | enable 8 | 15e07707-9815-4e77-aa15-22f94b192ae6 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/Extensions/Assert/KeyAsserts.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Keys; 2 | 3 | namespace Xunit; 4 | 5 | public partial class Assert 6 | { 7 | public static void KeysAreEqual(KeyVaultKey first, KeyVaultKey second) 8 | { 9 | Equal(first.Name, second.Name); 10 | Equal(first.Key.KeyType, second.Key.KeyType); 11 | Equal(first.Properties.Version, second.Properties.Version); 12 | Equal(first.Properties.VaultUri, second.Properties.VaultUri); 13 | } 14 | 15 | public static void KeysNotEqual(KeyVaultKey first, KeyVaultKey second) 16 | { 17 | NotEqual(first.Properties.Id, second.Properties.Id); 18 | NotEqual(first.Properties.Version, second.Properties.Version); 19 | } 20 | 21 | public static void KeyHasTag(KeyVaultKey key, string tagName, string expectedValue) 22 | { 23 | NotEmpty(key.Properties?.Tags); 24 | 25 | string? outValue = string.Empty; 26 | 27 | var exists = key.Properties?.Tags.TryGetValue(tagName, out outValue); 28 | 29 | if (exists is not null && !exists.Value) 30 | Fail($"No value for name: {tagName} found"); 31 | 32 | if (string.IsNullOrEmpty(outValue)) 33 | Fail($"Value for {tagName} was null or empty"); 34 | 35 | Equal(expectedValue, outValue); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.TestContainers.Tests/AzureKeyVaultEmulator.TestContainers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 27 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Certificates/Services/ICertificateBackingService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | using AzureKeyVaultEmulator.Shared.Models.Certificates; 3 | using AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 4 | using AzureKeyVaultEmulator.Shared.Models.Secrets; 5 | 6 | namespace AzureKeyVaultEmulator.Certificates.Services; 7 | 8 | public interface ICertificateBackingService 9 | { 10 | Task<(KeyBundle backingKey, SecretBundle backingSecret)> GetBackingComponentsAsync(string certName, X509Certificate2? cert, string? password = null, CertificatePolicy? policy = null, X509ContentType contentType = X509ContentType.Pfx); 11 | Task<(DeletedKeyBundle deletedBackingKey, DeletedSecretBundle deletedBackingSecret)> DeleteBackingComponentsAsync(string certName); 12 | 13 | Task GetIssuerAsync(string name); 14 | Task CreateIssuerAsync(string name, IssuerBundle bundle); 15 | Task AllocateIssuerToCertificateAsync(string certName, IssuerBundle bundle); 16 | 17 | Task UpdateCertificateIssuerAsync(string issuerName, IssuerBundle bundle); 18 | Task DeleteIssuerAsync(string issuerName); 19 | 20 | Task SetContactInformationAsync(SetContactsRequest request); 21 | Task DeleteCertificateContactsAsync(); 22 | Task GetCertificateContactsAsync(); 23 | } 24 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Middleware/RestoreDoubleSlashRerouteMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace AzureKeyVaultEmulator.Middleware; 4 | 5 | public static class BodgeExtensions 6 | { 7 | /// 8 | /// Bypasses the SDK bug causing API calls with an additional slash to fail. 9 | /// For example {vaultUri}/certificates/restore comes out of the SDK as {vaultUri}/certificates//restore, this middleware corrects that. 10 | /// PR raised here: https://github.com/Azure/azure-sdk-for-net/pull/49496 11 | /// 12 | /// 13 | public static WebApplication RegisterDoubleSlashBodge(this WebApplication application) 14 | { 15 | application.UseMiddleware(); 16 | application.UseRouting(); 17 | 18 | return application; 19 | } 20 | } 21 | 22 | public class RestoreDoubleSlashRerouteMiddleware(RequestDelegate next) 23 | { 24 | public async Task InvokeAsync(HttpContext context) 25 | { 26 | var originalPath = context.Request.Path.Value; 27 | 28 | if (originalPath is not null && originalPath.Contains("//")) 29 | { 30 | var rerouted = Regex.Replace(originalPath, "/{2,}", "/"); 31 | 32 | context.Request.Path = new PathString(rerouted); 33 | } 34 | 35 | await next(context); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Secrets/SecretBundle.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json.Serialization; 4 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 5 | 6 | namespace AzureKeyVaultEmulator.Shared.Models.Secrets; 7 | 8 | public sealed class SecretBundle : TaggedModel, INamedItem, IAttributedModel 9 | { 10 | [Key] 11 | [JsonIgnore] 12 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 13 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 14 | 15 | public string PersistedName { get; set; } = string.Empty; 16 | 17 | public string PersistedVersion { get; set; } = string.Empty; 18 | 19 | [JsonPropertyName("id")] 20 | public required string SecretIdentifier { get; set; } = string.Empty; 21 | 22 | [JsonPropertyName("value")] 23 | public string Value { get; set; } = string.Empty; 24 | 25 | [JsonPropertyName("contentType")] 26 | public string ContentType { get; set; } = string.Empty; 27 | 28 | [JsonPropertyName("managed")] 29 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 30 | public bool? Managed { get; set; } = null; 31 | 32 | [JsonPropertyName("attributes")] 33 | public SecretAttributes Attributes { get; set; } = new(); 34 | 35 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 36 | public bool Deleted { get; set; } = false; 37 | } 38 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/CertificateProperties.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using AzureKeyVaultEmulator.Shared.Constants; 3 | 4 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 5 | 6 | /// 7 | /// Encapsulates metadata about the certificate. 8 | /// Not listed on the API, very cool Azure, but throws if required items are missing. 9 | /// 10 | public class CertificateProperties : TaggedModel 11 | { 12 | [JsonPropertyName("id")] 13 | public required string CertificateIdentifier { get; set; } 14 | 15 | [JsonPropertyName("recoveryId")] 16 | public string RecoveryId { get; set; } = string.Empty; 17 | 18 | [JsonPropertyName("name")] 19 | public required string CertificateName { get; set; } 20 | 21 | [JsonPropertyName("vaultUri")] 22 | public required Uri VaultUri { get; set; } 23 | 24 | [JsonPropertyName("x5t")] 25 | public required string X509Thumbprint { get; set; } 26 | 27 | [JsonPropertyName("status")] 28 | public static string OperationStatus => OperationConstants.Completed; // del 29 | 30 | // Recovery not currently supported. Raise an issue if it's required please. 31 | [JsonPropertyName("recoveryLevelDays")] 32 | public int RecoveryLevelDays => 0; 33 | 34 | [JsonPropertyName("attributes")] 35 | public CertificateAttributes Attributes { get; set; } = new(); 36 | } 37 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Middleware/RequestDumpMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Http.Extensions; 5 | 6 | namespace AzureKeyVaultEmulator.Shared.Middleware; 7 | 8 | public sealed class RequestDumpMiddleware(RequestDelegate next) 9 | { 10 | public async Task InvokeAsync(HttpContext context) 11 | { 12 | context.Request.EnableBuffering(); 13 | 14 | context.Request.Body.Seek(0, SeekOrigin.Begin); 15 | 16 | using var sr = new StreamReader(context.Request.Body); 17 | var body = await sr.ReadToEndAsync(); 18 | 19 | context.Request.Body.Position = 0; 20 | 21 | RequestDebugModel dump = new() 22 | { 23 | Method = context.Request.Method, 24 | Path = context.Request.Host + context.Request.GetEncodedPathAndQuery(), 25 | Headers = context.Request.Headers.Select(x => $"Key: {x.Key}, Value: {x.Value}"), 26 | Body = body 27 | }; 28 | 29 | var json = JsonSerializer.Serialize(dump); 30 | 31 | Debug.WriteLine(json); 32 | 33 | await next(context); 34 | } 35 | } 36 | 37 | public sealed class RequestDebugModel 38 | { 39 | public string Method { get; set; } = string.Empty; 40 | public string Path { get; set; } = string.Empty; 41 | public IEnumerable Headers { get; set; } = []; 42 | public string Body { get; set; } = string.Empty; 43 | } 44 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/AzureKeyVaultEmulator.Aspire.Hosting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0;net10.0 4 | enable 5 | enable 6 | 7 | Azure Key Vault Emulator for .NET Aspire 8 | README.md 9 | James Gould - 2025 10 | https://github.com/james-gould/azure-keyvault-emulator 11 | https://github.com/james-gould/azure-keyvault-emulator 12 | Apache-2.0 13 | James Gould 14 | .NET Aspire AppHost support for the Azure KeyVault Emulator 15 | False 16 | keyvault, key vault, azure, .net aspire, aspire, aspire.host, aspire.hosting 17 | true 18 | c0273fdf-91dd-4646-856a-0f7cbcd39d13 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Provides the ability to emulate the `AzureKeyVault` Aspire resource using the open source [emulator](https://github.com/james-gould/azure-keyvault-emulator). 4 | 5 | Recommended, but not required, is the [client library](https://www.nuget.org/packages/AzureKeyVaultEmulator.Client) to make using the emulator in your applications incredibly simple. 6 | 7 | # Usage 8 | 9 | Install the package to your .NET Aspire `AppHost` project: 10 | 11 | ``` 12 | dotnet add package AzureKeyVaultEmulator.Aspire.Hosting 13 | ``` 14 | 15 | Next you can either redirect an existing `AzureKeyVaultResource` to use the emulator, or directly include it without needing any Azure configuration. 16 | 17 | To redirect an existing resource: 18 | 19 | ```csharp 20 | var keyVaultServiceName = "keyvault"; 21 | 22 | var keyVault = builder 23 | .AddAzureKeyVault(keyVaultServiceName) 24 | .RunAsEmulator(); // Add this line 25 | 26 | var webApi = builder 27 | .AddProject("api") 28 | .WithReference(keyvault); // reference as normal 29 | ``` 30 | 31 | To use directly without needing to set up any Azure configuration: 32 | 33 | ```csharp 34 | var keyVaultServiceName = "keyvault"; 35 | 36 | var keyVault = builder.AddAzureKeyVaultEmulator(keyVaultServiceName); 37 | ``` 38 | 39 | You will then have a feature complete, emulated `Azure Key Vault` running locally: 40 | 41 | ![Azure Key Vault Emulator in .NET Aspire](https://i.imgur.com/gMpfwrN.png) -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/ApiConfiguration/SwaggerGeneration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.OpenApi.Models; 3 | 4 | namespace AzureKeyVaultEmulator.ApiConfiguration 5 | { 6 | public static class SwaggerGeneration 7 | { 8 | public static IServiceCollection AddConfiguredSwaggerGen(this IServiceCollection services) 9 | { 10 | services.AddSwaggerGen(c => 11 | { 12 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "Azure KeyVault Emulator", Version = "v1" }); 13 | c.AddSecurityDefinition("JWT", new OpenApiSecurityScheme 14 | { 15 | Name = "Authorization", 16 | In = ParameterLocation.Header, 17 | Type = SecuritySchemeType.Http, 18 | Description = "JWT Authorization header using the Bearer scheme. Use '/token to generate a token", 19 | Scheme = JwtBearerDefaults.AuthenticationScheme, 20 | }); 21 | c.AddSecurityRequirement(new OpenApiSecurityRequirement 22 | { 23 | { 24 | new OpenApiSecurityScheme 25 | { 26 | Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "JWT" } 27 | }, 28 | Array.Empty() 29 | } 30 | }); 31 | }); 32 | 33 | return services; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Client/AzureKeyVaultEmulator.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Client support for the Azure Key Vault Emulator 6 | Registers and injects the Azure Clients for Azure Key Vault, targetting the Azure Key Vault emulator. 7 | README.md 8 | James Gould - 2025 9 | https://github.com/james-gould/azure-keyvault-emulator 10 | https://github.com/james-gould/azure-keyvault-emulator 11 | James Gould 12 | keyvault, key vault, azure, .net aspire, aspire, aspire.client 13 | Apache-2.0 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/Requests/CertificateContacts.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using AzureKeyVaultEmulator.Shared.Constants; 6 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 7 | 8 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 9 | 10 | public sealed class CertificateContacts : INamedItem 11 | { 12 | [Key] 13 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 14 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 15 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 16 | 17 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 18 | public string PersistedVersion { get; set; } = string.Empty; 19 | 20 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 21 | public string PersistedName { get; set; } = string.Empty; 22 | 23 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 24 | public bool Deleted { get; set; } = false; 25 | 26 | [JsonPropertyName("id")] 27 | public string Id { get; } = $"{AuthConstants.EmulatorUri}/certificates/contacts"; 28 | 29 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 30 | public string BackingContacts { get; set; } = "[]"; 31 | 32 | [JsonPropertyName("contacts")] 33 | [NotMapped] 34 | public IEnumerable Contacts 35 | { 36 | get => JsonSerializer.Deserialize>(BackingContacts) ?? []; 37 | set => BackingContacts = JsonSerializer.Serialize(value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Program.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.ApiConfiguration; 2 | using AzureKeyVaultEmulator.Middleware; 3 | using AzureKeyVaultEmulator.Shared.Middleware; 4 | using AzureKeyVaultEmulator.Shared.Persistence; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | builder.AddServiceDefaults(); 10 | 11 | builder.Services.AddConfiguredAuthentication(); 12 | 13 | builder.Services.AddControllers(); 14 | 15 | builder.Services.AddHttpContextAccessor(); 16 | 17 | builder.Services.AddEndpointsApiExplorer(); 18 | builder.Services.AddSwaggerGen(); 19 | 20 | //builder.Services.AddConfiguredSwaggerGen(); 21 | builder.Services.RegisterCustomServices(); 22 | 23 | // Registers the SQLite database, respecting choice around persisted on disk. 24 | builder.Services.AddVaultPersistenceLayer(); 25 | 26 | var app = builder.Build(); 27 | 28 | app.RegisterDoubleSlashBodge(); 29 | 30 | if (app.Environment.IsDevelopment()) 31 | { 32 | app.UseDeveloperExceptionPage(); 33 | app.UseSwagger(); 34 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Azure Key Vault Emulator")); 35 | 36 | app.UseMiddleware(); 37 | } 38 | 39 | app.UseHttpsRedirection(); 40 | app.UseForwardedHeaders(); 41 | app.UseMiddleware(); 42 | app.UseMiddleware(); 43 | 44 | using var scope = app.Services.CreateScope(); 45 | var db = scope.ServiceProvider.GetRequiredService(); 46 | await db.Database.MigrateAsync(); 47 | 48 | app.UseAuthentication(); 49 | app.UseAuthorization(); 50 | 51 | app.MapControllers(); 52 | 53 | app.Run(); 54 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/AzureKeyVaultEmulator.TestContainers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1;net8.0;net9.0;net10.0 5 | enable 6 | 8.0 7 | 8 | Azure Key Vault Emulator TestContainers Module 9 | README.md 10 | James Gould - 2025 11 | https://github.com/james-gould/azure-keyvault-emulator 12 | https://github.com/james-gould/azure-keyvault-emulator 13 | Apache-2.0 14 | James Gould 15 | TestContainers module for the Azure Key Vault Emulator 16 | False 17 | testcontainers, azure, keyvault, emulator, testing, docker 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-all-arm-manual.yml: -------------------------------------------------------------------------------- 1 | name: Publish ARM Docker Images 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Build and Test"] 6 | types: 7 | - completed 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-24.04-arm 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Fetch all tags 21 | run: git fetch --tags 22 | 23 | - name: Get latest tag 24 | id: get_version 25 | run: | 26 | TAG=$(git tag -l "v*.*.*" | sort -V | tail -n 1) 27 | if [[ -z "$TAG" ]]; then 28 | echo "No Git tag found. Please create a version tag before merging the PR." 29 | exit 1 30 | fi 31 | echo "VERSION=${TAG#v}" >> $GITHUB_ENV 32 | 33 | - name: Log in to Container Registry 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ${{ secrets.REGISTRY }} 37 | username: ${{ secrets.USERNAME }} 38 | password: ${{ secrets.PASSWORD }} 39 | 40 | - name: Build Docker Image 41 | run: | 42 | docker build --platform linux/arm64/v8 -t jamesgoulddev/azure-keyvault-emulator:latest-arm -t jamesgoulddev/azure-keyvault-emulator:${{ env.VERSION }}-arm . 43 | 44 | - name: Push Latest Docker Image (ARM64) 45 | run: docker push jamesgoulddev/azure-keyvault-emulator:latest-arm 46 | 47 | - name: Push Versioned Docker Image (ARM64) 48 | run: docker push jamesgoulddev/azure-keyvault-emulator:${{ env.VERSION }}-arm 49 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Persistence/Utils/RsaParametersSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Persistence.Utils; 4 | 5 | public static class RsaParametersSerializer 6 | { 7 | public static byte[] Serialize(RSAParameters p) 8 | { 9 | using var ms = new MemoryStream(); 10 | using var writer = new BinaryWriter(ms); 11 | 12 | Write(p.D); 13 | Write(p.DP); 14 | Write(p.DQ); 15 | Write(p.Exponent); 16 | Write(p.InverseQ); 17 | Write(p.Modulus); 18 | Write(p.P); 19 | Write(p.Q); 20 | 21 | return ms.ToArray(); 22 | 23 | void Write(byte[]? data) 24 | { 25 | if (data == null) 26 | { 27 | writer.Write(-1); 28 | } 29 | else 30 | { 31 | writer.Write(data.Length); 32 | writer.Write(data); 33 | } 34 | } 35 | } 36 | 37 | public static RSAParameters Deserialize(byte[] data) 38 | { 39 | using var ms = new MemoryStream(data); 40 | using var reader = new BinaryReader(ms); 41 | 42 | return new RSAParameters 43 | { 44 | D = Read(), 45 | DP = Read(), 46 | DQ = Read(), 47 | Exponent = Read(), 48 | InverseQ = Read(), 49 | Modulus = Read(), 50 | P = Read(), 51 | Q = Read() 52 | }; 53 | 54 | byte[]? Read() 55 | { 56 | int len = reader.ReadInt32(); 57 | return len == -1 ? null : reader.ReadBytes(len); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/SetupHelper/Fixtures/SecretsTestingFixture.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Secrets; 2 | 3 | namespace AzureKeyVaultEmulator.IntegrationTests.SetupHelper.Fixtures; 4 | 5 | public class SecretsTestingFixture : KeyVaultClientTestingFixture 6 | { 7 | private SecretClient? _secretClient; 8 | 9 | private KeyVaultSecret? _defaultSecret = null; 10 | 11 | public override async ValueTask GetClientAsync() 12 | { 13 | if (_secretClient is not null) 14 | return _secretClient; 15 | 16 | var options = new SecretClientOptions 17 | { 18 | DisableChallengeResourceVerification = true, 19 | RetryPolicy = _clientRetryPolicy 20 | }; 21 | 22 | var setupModel = await GetClientSetupModelAsync(); 23 | 24 | return _secretClient = new SecretClient(setupModel.VaultUri, setupModel.Credential, options); 25 | } 26 | 27 | public async ValueTask CreateSecretAsync() 28 | { 29 | if (_defaultSecret is not null) 30 | return _defaultSecret; 31 | 32 | ArgumentNullException.ThrowIfNull(_secretClient); 33 | 34 | return _defaultSecret = await _secretClient.SetSecretAsync(FreshlyGeneratedGuid, FreshlyGeneratedGuid); 35 | } 36 | 37 | public async Task CreateSecretAsync(string secretName, string secretValue) 38 | { 39 | ArgumentException.ThrowIfNullOrWhiteSpace(secretName); 40 | ArgumentNullException.ThrowIfNull(secretValue); 41 | 42 | _secretClient = await GetClientAsync(); 43 | 44 | return await _secretClient.SetSecretAsync(secretName, secretValue); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/Helpers/AzureKeyVaultEmulatorClientHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.Security.KeyVault.Secrets; 3 | 4 | namespace AzureKeyVaultEmulator.Aspire.Hosting.Helpers; 5 | 6 | internal static class AzureKeyVaultEmulatorClientHelper 7 | { 8 | internal static SecretClient GetSecretClient(string vaultUri) 9 | { 10 | var opt = new SecretClientOptions { DisableChallengeResourceVerification = true }; 11 | 12 | var uri = new Uri(vaultUri); 13 | 14 | var credential = new EmulatedTokenCredential(uri); 15 | 16 | return new SecretClient(uri, credential, opt); 17 | } 18 | 19 | private class EmulatedTokenCredential(Uri vaultUri) : TokenCredential 20 | { 21 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 22 | => GetTokenAsync(requestContext, cancellationToken).AsTask().GetAwaiter().GetResult(); 23 | 24 | public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 25 | { 26 | using var client = new HttpClient(); 27 | 28 | try 29 | { 30 | client.BaseAddress = vaultUri; 31 | 32 | var response = await client.GetAsync("token"); 33 | 34 | var content = await response.Content.ReadAsStringAsync(); 35 | 36 | return new AccessToken(content, DateTimeOffset.Now.AddYears(1)); 37 | } 38 | catch 39 | { 40 | throw; 41 | } 42 | finally 43 | { 44 | client?.Dispose(); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Keys/Controllers/DeletedKeysController.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Keys.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace AzureKeyVaultEmulator.Keys.Controllers; 6 | 7 | [ApiController] 8 | [Authorize] 9 | public class DeletedKeysController(IKeyService keyService, ITokenService tokenService) : Controller 10 | { 11 | [HttpGet("deletedkeys/{name}")] 12 | public async Task GetDeletedKey( 13 | [FromRoute] string name, 14 | [ApiVersion] string apiVersion) 15 | { 16 | var result = await keyService.GetDeletedKeyAsync(name); 17 | 18 | return Ok(result); 19 | } 20 | 21 | [HttpGet("deletedkeys")] 22 | public IActionResult GetDeletedKeys( 23 | [ApiVersion] string apiVersion, 24 | [FromQuery] int maxResults = 25, 25 | [SkipToken] string skipToken = "") 26 | { 27 | int skipCount = 0; 28 | 29 | if(!string.IsNullOrEmpty(skipToken)) 30 | skipCount = tokenService.DecodeSkipToken(skipToken); 31 | 32 | var result = keyService.GetDeletedKeys(maxResults, skipCount); 33 | 34 | return Ok(result); 35 | } 36 | 37 | [HttpDelete("deletedkeys/{name}")] 38 | public async Task PurgeDeletedKey( 39 | [FromRoute] string name, 40 | [ApiVersion] string apiVersion) 41 | { 42 | await keyService.PurgeDeletedKey(name); 43 | 44 | return NoContent(); 45 | } 46 | 47 | [HttpPost("deletedkeys/{name}/recover")] 48 | public async Task RecoverDeletedKey( 49 | [FromRoute] string name, 50 | [ApiVersion] string apiVersion) 51 | { 52 | var result = await keyService.RecoverDeletedKeyAsync(name); 53 | 54 | return Ok(result); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/AzureKeyVaultEmulator.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net10.0 4 | enable 5 | enable 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/SetupHelper/Fixtures/KeysTestingFixture.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Keys; 2 | using Azure.Security.KeyVault.Keys.Cryptography; 3 | 4 | namespace AzureKeyVaultEmulator.IntegrationTests.SetupHelper.Fixtures; 5 | 6 | public sealed class KeysTestingFixture : KeyVaultClientTestingFixture 7 | { 8 | private KeyClient? _client; 9 | 10 | public const string DefaultKeyName = "algKey"; 11 | private KeyType _defaultType = KeyType.Rsa; 12 | 13 | public override async ValueTask GetClientAsync() 14 | { 15 | if (_client is not null) 16 | return _client; 17 | 18 | var setup = await GetClientSetupModelAsync(); 19 | 20 | var opt = new KeyClientOptions 21 | { 22 | DisableChallengeResourceVerification = true, 23 | RetryPolicy = _clientRetryPolicy 24 | }; 25 | 26 | return _client = new KeyClient(setup.VaultUri, setup.Credential, opt); 27 | } 28 | 29 | public async Task GetCryptographyClientAsync(KeyVaultKey key) 30 | { 31 | ArgumentNullException.ThrowIfNull(key); 32 | 33 | var bearer = await GetBearerTokenAsync(); 34 | 35 | var cred = new EmulatedTokenCredential(bearer); 36 | 37 | return new CryptographyClient(key.Id, cred); 38 | } 39 | 40 | public async Task CreateKeyAsync(string name = DefaultKeyName, KeyType? type = null) 41 | { 42 | var client = await GetClientAsync(); 43 | 44 | type ??= _defaultType; 45 | 46 | var result = await client.CreateKeyAsync(name, type.Value); 47 | 48 | if(result?.Value is null) 49 | throw new InvalidOperationException($"Failed to create Key in {nameof(KeysTestingFixture)}"); 50 | 51 | return result.Value; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/AttributeBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json.Serialization; 4 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 5 | 6 | namespace AzureKeyVaultEmulator.Shared.Models 7 | { 8 | public class AttributeBase : IPersistedItem 9 | { 10 | public AttributeBase() 11 | { 12 | var now = DateTimeOffset.Now; 13 | 14 | Created = now.ToUnixTimeSeconds(); 15 | Updated = now.ToUnixTimeSeconds(); 16 | NotBefore = now.ToUnixTimeSeconds(); 17 | Expiration = now.AddDays(365).ToUnixTimeSeconds(); 18 | } 19 | 20 | [Key] 21 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 22 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 23 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 24 | 25 | [JsonPropertyName("enabled")] 26 | public bool Enabled { get; set; } = true; 27 | 28 | [JsonPropertyName("exp")] 29 | public long Expiration { get; set; } 30 | 31 | [JsonPropertyName("nbf")] 32 | public long NotBefore { get; set; } 33 | 34 | [JsonPropertyName("created")] 35 | public long Created { get; set; } 36 | 37 | [JsonPropertyName("updated")] 38 | public long Updated { get; set; } 39 | 40 | [JsonPropertyName("recoveryLevel")] 41 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 42 | public string? RecoveryLevel { get; set; } = DeletionRecoveryLevel.RecoverablePurgeable; 43 | 44 | [JsonPropertyName("recoverableDays")] 45 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 46 | public int? RecoverableDays { get; set; } = 90; 47 | 48 | public void Update() => Updated = DateTimeOffset.Now.ToUnixTimeSeconds(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################### 2 | # compiled source # 3 | ################### 4 | *.com 5 | *.class 6 | *.dll 7 | *.exe 8 | *.pdb 9 | *.dll.config 10 | *.cache 11 | *.suo 12 | # Include dlls if they’re in the NuGet packages directory 13 | !/packages/*/lib/*.dll 14 | !/packages/*/lib/*/*.dll 15 | # Include dlls if they're in the CommonReferences directory 16 | !*CommonReferences/*.dll 17 | #################### 18 | # VS Upgrade stuff # 19 | #################### 20 | UpgradeLog.XML 21 | _UpgradeReport_Files/ 22 | ############### 23 | # Directories # 24 | ############### 25 | bin/ 26 | obj/ 27 | TestResults/ 28 | ################### 29 | # Web publish log # 30 | ################### 31 | *.Publish.xml 32 | ############# 33 | # Resharper # 34 | ############# 35 | /_ReSharper.* 36 | *.ReSharper.* 37 | ############ 38 | # Packages # 39 | ############ 40 | # it’s better to unpack these files and commit the raw source 41 | # git has its own built in compression methods 42 | *.7z 43 | *.dmg 44 | *.gz 45 | *.iso 46 | *.jar 47 | *.rar 48 | *.tar 49 | *.zip 50 | ###################### 51 | # Logs and databases # 52 | ###################### 53 | *.log 54 | *.sqlite 55 | # OS generated files # 56 | ###################### 57 | .DS_Store? 58 | .DS_Store 59 | ehthumbs.db 60 | Icon? 61 | Thumbs.db 62 | [Bb]in 63 | [Oo]bj 64 | [Tt]est[Rr]esults 65 | *.suo 66 | *.user 67 | *.[Cc]ache 68 | *[Rr]esharper* 69 | packages 70 | NuGet.exe 71 | _[Ss]cripts 72 | *.exe 73 | *.dll 74 | *.nupkg 75 | *.ncrunchsolution 76 | *.dot[Cc]over 77 | *.idea/ 78 | *.DS_Store 79 | **/wwwroot/lib/* 80 | **/wwwroot/dist/* 81 | .vs/ 82 | node_modules/ 83 | ci/pipeline-secrets.yml 84 | tf/.terraform/ 85 | tf/terraform.tfstate.d/ 86 | .vscode 87 | out/ 88 | *.txt 89 | 90 | publish/ 91 | sftp/ 92 | newrelic/ 93 | .idea/ 94 | 95 | *.crt 96 | *.pfx 97 | *.key 98 | 99 | # local development script for being lazy 100 | docker-build.ps1 101 | 102 | # Fake GraphQL directives # 103 | ########################### 104 | _not-imported-dev.graphql 105 | 106 | .aspire/* -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/CertificateOperation.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using AzureKeyVaultEmulator.Shared.Constants; 3 | using AzureKeyVaultEmulator.Shared.Utilities; 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 6 | 7 | // https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/create-certificate/create-certificate?view=rest-keyvault-certificates-7.4&tabs=HTTP#certificateoperation 8 | public sealed class CertificateOperation(string id, string certName) 9 | { 10 | [JsonPropertyName("cancellation_requested")] 11 | public bool CancellationRequested { get; set; } = false; 12 | 13 | [JsonPropertyName("csr")] 14 | public string CertificateSigningRequest { get; set; } = ""; // Can't see a way to pass a signing cert/csr? 15 | 16 | //[JsonPropertyName("error")] 17 | //public KeyVaultError? Error { get; set; } = new(); // Might be worth piping this in if something does go wrong? 18 | 19 | [JsonPropertyName("id")] 20 | public string CertificateIdentifier { get; set; } = id; 21 | 22 | [JsonPropertyName("issuer")] 23 | public IssuerParameters IssuerParameters { get; set; } = new(); 24 | 25 | [JsonPropertyName("request_id")] 26 | public string RequestId { get; set; } = Guid.NewGuid().Neat(); 27 | 28 | [JsonPropertyName("status")] 29 | public string Status { get; set; } = OperationConstants.Completed; 30 | 31 | [JsonPropertyName("status_details")] 32 | public string StatusDetails { get; set; } = OperationConstants.CompletedReason; 33 | 34 | [JsonPropertyName("target")] 35 | public string Target { get; set; } = $"/certificates/{certName}"; 36 | } 37 | 38 | public sealed class IssuerParameters 39 | { 40 | [JsonPropertyName("cert_transparency")] 41 | public bool Transparency { get; set;} = true; 42 | 43 | [JsonPropertyName("cty")] 44 | public string CertificateType { get; set; } = ""; // Optional, not concerned about it yet. 45 | 46 | [JsonPropertyName("name")] 47 | public string Name { get; set; } = "AzureKeyVaultEmulator-SelfSigned"; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Middleware/KeyVaultErrorMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace AzureKeyVaultEmulator.Middleware 4 | { 5 | public class KeyVaultErrorMiddleware(RequestDelegate next) 6 | { 7 | public async Task InvokeAsync(HttpContext context) 8 | { 9 | try 10 | { 11 | await next(context); 12 | } 13 | catch (Exception e) 14 | { 15 | var req = context.Request; 16 | 17 | var error = new KeyVaultError 18 | { 19 | Code = "Failed to perform request into Azure Key Vault Emulator", 20 | InnerError = e.InnerException == null ? null : new KeyVaultError 21 | { 22 | Message = e.InnerException.Message, 23 | }, 24 | Message = e.Message 25 | }; 26 | var status = HttpStatusCode.BadRequest; 27 | 28 | if (e is ConflictedItemException conflictedItemException) 29 | { 30 | error = new KeyVaultError() 31 | { 32 | Code = "Conflict", 33 | Message = $"{conflictedItemException.ItemType} {conflictedItemException.Name} is currently in a deleted but recoverable state, and its name cannot be reused; in this state, the key can only be recovered or purged.", 34 | InnerError = new KeyVaultError 35 | { 36 | Code = "ObjectIsDeletedButRecoverable" 37 | }, 38 | }; 39 | status = HttpStatusCode.Conflict; 40 | } 41 | else if (e is MissingItemException) 42 | { 43 | status = HttpStatusCode.NotFound; 44 | } 45 | 46 | context.Response.StatusCode = (int)status; 47 | await context.Response.WriteAsJsonAsync(new 48 | { 49 | Error = error 50 | }); 51 | 52 | return; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Certificates/Services/ICertificateService.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Shared.Models.Certificates; 2 | using AzureKeyVaultEmulator.Shared.Models.Certificates.Requests; 3 | using AzureKeyVaultEmulator.Shared.Models.Secrets; 4 | 5 | namespace AzureKeyVaultEmulator.Certificates.Services; 6 | 7 | public interface ICertificateService 8 | { 9 | Task CreateCertificateAsync(string name, CertificateAttributes attributes, CertificatePolicy? policy, Dictionary? tags = null); 10 | Task GetCertificateAsync(string name, string version = ""); 11 | ListResult GetCertificates(int maxResults = 25, int skipToken = 25); 12 | Task> GetCertificateVersionsAsync(string name, int maxResults = 25, int skipCount = 25); 13 | 14 | Task GetPendingCertificateAsync(string name); 15 | Task UpdateCertificateAsync(string name, string? version, UpdateCertificateRequest request); 16 | Task UpdateCertificatePolicyAsync(string name, CertificatePolicy certificatePolicy); 17 | Task GetCertificatePolicyAsync(string name); 18 | Task GetCertificateIssuerAsync(string name); 19 | 20 | Task> BackupCertificateAsync(string name); 21 | Task RestoreCertificateAsync(ValueModel backup); 22 | Task ImportCertificateAsync(string name, ImportCertificateRequest request); 23 | Task MergeCertificatesAsync(string name, MergeCertificatesRequest request); 24 | 25 | Task GetDeletedCertificateAsync(string name); 26 | Task GetPendingDeletedCertificateAsync(string name); 27 | 28 | Task DeleteCertificateAsync(string name); 29 | Task> GetDeletedCertificatesAsync(int maxResults = 25, int skipCount = 25); 30 | 31 | Task GetPendingRecoveryOperationAsync(string name); 32 | Task RecoverCerticateAsync(string name); 33 | Task PurgeDeletedCertificateAsync(string name); 34 | } 35 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/QueryUtils.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Shared.Models; 2 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Utilities; 6 | 7 | public static class QueryUtils 8 | { 9 | /// 10 | /// Returns a collection of entities representing the latest version for each unique persisted name in the set. 11 | /// 12 | /// The latest version is determined by selecting the entity with the latest 'Created' 13 | /// timestamp in its attributes for each unique persisted name. This method is typically used to identify the 14 | /// latest state of entities that may have multiple versions. 15 | /// The type of entity contained in the set. Must implement both IAttributedModel{TAttribute} and INamedItem. 16 | /// The type of attribute associated with each entity. Must derive from AttributeBase. 17 | /// The DbSet containing the entities to evaluate. Cannot be null. 18 | /// An enumerable collection of entities, each representing the latest version for a given persisted name. If no 19 | /// entities are present, the collection will be empty. 20 | public static IQueryable GetLatestVersions(this DbSet items) 21 | where TEntity : class, IAttributedModel, INamedItem 22 | where TAttribute : AttributeBase 23 | { 24 | var minima = 25 | items 26 | .Where(x => x.Deleted == false) 27 | .GroupBy(x => x.PersistedName) 28 | .Select(g => new 29 | { 30 | Name = g.Key, 31 | MaxCreated = g.Max(x => x.Attributes.Created) 32 | }); 33 | 34 | return items 35 | .Join( 36 | minima, 37 | item => new { item.PersistedName, item.Attributes.Updated }, 38 | m => new { PersistedName = m.Name, Updated = m.MaxCreated }, 39 | (item, _) => item 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/SetupHelper/RequestSetup.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using AzureKeyVaultEmulator.Shared.Utilities; 3 | 4 | namespace AzureKeyVaultEmulator.IntegrationTests.SetupHelper 5 | { 6 | public static class RequestSetup 7 | { 8 | /// 9 | /// Executes between and times. 10 | /// 11 | /// The the underlying KeyVault type to execute a request for. 12 | /// The lower bound for execution count. 13 | /// The upper limit for execution count. 14 | /// The func to execute. 15 | /// The underlying name of the response type. 16 | public static async Task CreateMultiple( 17 | int lower, int upper, 18 | Func>> execution) 19 | { 20 | var executionCount = Random.Shared.Next(lower, upper); 21 | 22 | var tasks = Enumerable 23 | .Range(0, executionCount) 24 | .Select(i => execution(i)); 25 | 26 | await Task.WhenAll(tasks); 27 | 28 | return executionCount; 29 | } 30 | 31 | public static async Task CreateMultiple( 32 | int lower, int upper, 33 | Func> execution) 34 | { 35 | var executionCount = Random.Shared.Next(lower, upper); 36 | 37 | var tasks = Enumerable 38 | .Range(0, executionCount) 39 | .Select(i => execution(i)); 40 | 41 | await Task.WhenAll(tasks); 42 | 43 | return executionCount; 44 | } 45 | 46 | public static string CreateRandomData(int size = 512) 47 | { 48 | var bytes = CreateRandomBytes(size); 49 | 50 | return EncodingUtils.Base64UrlEncode(bytes); 51 | } 52 | 53 | public static byte[] CreateRandomBytes(int size = 512) 54 | { 55 | byte[] bytes = new byte[size]; 56 | 57 | Random.Shared.NextBytes(bytes); 58 | 59 | return bytes; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /dev/WebApiWithEmulator.DebugHelper/WebApiPWithEmulator/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Certificates; 2 | using AzureKeyVaultEmulator.Aspire.Client; 3 | using AzureKeyVaultEmulator.Shared.Constants.Orchestration; 4 | using AzureKeyVaultEmulator.Shared.Utilities; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | builder.AddServiceDefaults(); 10 | 11 | // Add services to the container. 12 | 13 | builder.Services.AddControllers(); 14 | builder.Services.AddEndpointsApiExplorer(); 15 | builder.Services.AddSwaggerGen(); 16 | 17 | var vaultUri = builder.Configuration.GetConnectionString(AspireConstants.EmulatorServiceName); 18 | 19 | builder.Services.AddAzureKeyVaultEmulator(vaultUri, secrets: true, certificates: true, keys: true); 20 | 21 | var wiremock = Environment.GetEnvironmentVariable(AspireConstants.Wiremock); 22 | 23 | if (!string.IsNullOrEmpty(wiremock)) 24 | { 25 | builder.Services.AddHttpClient(WiremockConstants.HttpClientName, (sp, client) => 26 | { 27 | client.BaseAddress = new Uri(wiremock); 28 | }); 29 | } 30 | 31 | var app = builder.Build(); 32 | 33 | app.MapDefaultEndpoints(); 34 | 35 | // Configure the HTTP request pipeline. 36 | if (app.Environment.IsDevelopment()) 37 | { 38 | app.UseSwagger(); 39 | app.UseSwaggerUI(); 40 | } 41 | 42 | app.UseHttpsRedirection(); 43 | 44 | app.UseAuthorization(); 45 | 46 | app.MapControllers(); 47 | 48 | app.MapGet(WiremockConstants.EndpointPath, async ([FromServices] IHttpClientFactory httpClientFactory) => 49 | { 50 | try 51 | { 52 | var client = httpClientFactory.CreateClient(WiremockConstants.HttpClientName); 53 | 54 | var response = await client.GetAsync(WiremockConstants.EndpointName); 55 | 56 | var content = await response.Content.ReadAsStringAsync(); 57 | 58 | return content; 59 | } 60 | catch (Exception) 61 | { 62 | throw; 63 | } 64 | }); 65 | 66 | app.MapGet("/", async ([FromServices] CertificateClient client) => 67 | { 68 | var certName = Guid.NewGuid().Neat(); 69 | 70 | var op = await client.StartCreateCertificateAsync(certName, CertificatePolicy.Default); 71 | 72 | await op.WaitForCompletionAsync(); 73 | 74 | var cert = await client.GetCertificateAsync(certName); 75 | 76 | return certName; 77 | }); 78 | 79 | app.Run(); 80 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/Certificates/CertificateContactTests.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Certificates; 2 | using AzureKeyVaultEmulator.IntegrationTests.SetupHelper.Fixtures; 3 | 4 | namespace AzureKeyVaultEmulator.IntegrationTests.Certificates; 5 | 6 | public class CertificateContactTests(CertificatesTestingFixture fixture) : IClassFixture 7 | { 8 | [Fact] 9 | public async Task SetCertificateContactsWillPersistInStore() 10 | { 11 | await TryDeleteExistingContacts(); 12 | 13 | var client = await fixture.GetClientAsync(); 14 | 15 | var contacts = await SetContactsAsync(); 16 | 17 | var fromStore = await client.GetContactsAsync(); 18 | 19 | Assert.Equivalent(Contacts, fromStore?.Value); 20 | } 21 | 22 | [Fact] 23 | public async Task DeleteCertificateContactsWillRemoveFromstore() 24 | { 25 | await TryDeleteExistingContacts(); 26 | 27 | var client = await fixture.GetClientAsync(); 28 | 29 | var contacts = await SetContactsAsync(); 30 | 31 | var fromStore = await client.GetContactsAsync(); 32 | 33 | Assert.NotNull(fromStore); 34 | 35 | Assert.Equivalent(fromStore?.Value, contacts); 36 | 37 | var deleteResponse = await client.DeleteContactsAsync(); 38 | 39 | Assert.NotNull(deleteResponse.Value); 40 | 41 | var contactsAfterDelete = await client.GetContactsAsync(); 42 | 43 | Assert.NotNull(contactsAfterDelete.Value); 44 | 45 | Assert.Empty(contactsAfterDelete.Value); 46 | } 47 | 48 | private async Task TryDeleteExistingContacts() 49 | { 50 | try 51 | { 52 | var client = await fixture.GetClientAsync(); 53 | await client.DeleteContactsAsync(); 54 | } 55 | catch { } 56 | } 57 | 58 | private async Task> SetContactsAsync() 59 | { 60 | var client = await fixture.GetClientAsync(); 61 | 62 | var response = await client.SetContactsAsync(Contacts); 63 | 64 | Assert.NotNull(response.Value); 65 | 66 | Assert.Equivalent(response.Value, Contacts); 67 | 68 | return response.Value; 69 | } 70 | 71 | private List Contacts 72 | => [ new() { Email = "test@test.com", Name = "myName", Phone = "+00 000 000 000" } ]; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/ApiConfiguration/AuthenticationSetup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.IdentityModel.JsonWebTokens; 3 | 4 | namespace AzureKeyVaultEmulator.ApiConfiguration 5 | { 6 | public static class AuthenticationSetup 7 | { 8 | /// 9 | /// We just want to force requests through, the client libraries expect auth but we don't care about it here. 10 | /// 11 | /// 12 | /// 13 | public static IServiceCollection AddConfiguredAuthentication(this IServiceCollection services) 14 | { 15 | services 16 | .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 17 | .AddJwtBearer(options => 18 | { 19 | options.TokenValidationParameters = new TokenValidationParameters 20 | { 21 | ValidateIssuer = false, 22 | ValidateAudience = false, 23 | ValidateLifetime = false, 24 | ValidateIssuerSigningKey = false, 25 | 26 | ValidIssuers = [AuthConstants.EmulatorIss], 27 | ValidAudiences = [AuthConstants.EmulatorIss], 28 | IssuerSigningKey = AuthConstants.SigningKey, 29 | SignatureValidator = (token, parameters) => new JsonWebToken(token), 30 | }; 31 | 32 | options.Events = new JwtBearerEvents 33 | { 34 | OnChallenge = context => 35 | { 36 | var requestHostSplit = context.Request.Host.ToString().Split(".", 2); 37 | var scope = $"https://{requestHostSplit[^1]}/.default"; 38 | context.Response.Headers.Remove("WWW-Authenticate"); 39 | context.Response.Headers.WWWAuthenticate = $"Bearer authorization=\"{AuthConstants.EmulatorUri}{context.Request.Path}\", scope=\"{scope}\", resource=\"https://vault.azure.net\""; 40 | 41 | return Task.CompletedTask; 42 | } 43 | }; 44 | }); 45 | 46 | services.AddAuthorization(); 47 | 48 | return services; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/CertificatePolicy.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using AzureKeyVaultEmulator.Shared.Models.Keys; 6 | using AzureKeyVaultEmulator.Shared.Models.Secrets; 7 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 8 | using AzureKeyVaultEmulator.Shared.Utilities; 9 | 10 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 11 | 12 | public sealed class CertificatePolicy : INamedItem 13 | { 14 | [Key] 15 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 16 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 17 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 18 | 19 | public string PersistedName { get; set; } = Guid.NewGuid().Neat(); 20 | 21 | public string PersistedVersion { get; set; } = Guid.NewGuid().Neat(); 22 | 23 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 24 | public Guid ParentCertificateId { get; set; } 25 | 26 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 27 | public Guid IssuerId { get; set; } 28 | 29 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 30 | public bool Deleted { get; set; } = false; 31 | 32 | [JsonPropertyName("id")] 33 | public string Identifier { get; set; } = string.Empty; 34 | 35 | [JsonPropertyName("issuer")] 36 | public IssuerBundle Issuer { get; set; } = new(); 37 | 38 | [JsonPropertyName("attributes")] 39 | public CertificateAttributes CertificateAttributes { get; set; } = new(); 40 | 41 | [JsonPropertyName("x509_props")] 42 | public X509CertificateProperties? CertificateProperties { get; set; } = new(); 43 | 44 | public string BackingLifetimeActions { get; set; } = "[]"; 45 | 46 | [JsonPropertyName("lifetime_actions")] 47 | [NotMapped] 48 | public IEnumerable LifetimeActions 49 | { 50 | get => JsonSerializer.Deserialize>(BackingLifetimeActions) ?? []; 51 | } 52 | 53 | [JsonPropertyName("key_props")] 54 | public KeyProperties? KeyProperties { get; set; } = new(); 55 | 56 | [JsonPropertyName("secret_props")] 57 | public SecretProperties? SecretProperies { get; set; } = new(); 58 | 59 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 60 | public CertificateBundle? CertificateBundle { get; set; } = null; 61 | } 62 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Client/EmulatedTokenCredential.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Azure.Core; 6 | 7 | namespace AzureKeyVaultEmulator.Aspire.Client 8 | { 9 | public sealed class EmulatedTokenCredential : TokenCredential 10 | { 11 | public EmulatedTokenCredential(string vaultUri) 12 | { 13 | _emulatedVaultUri = vaultUri; 14 | } 15 | 16 | private string _emulatedVaultUri = string.Empty; 17 | private string _token = string.Empty; 18 | private DateTimeOffset _expiry => DateTimeOffset.Now.AddDays(1); 19 | 20 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 21 | { 22 | // Hate this but someone somewhere will be using Sync methods... 23 | var token = GetBearerToken().GetAwaiter().GetResult(); 24 | 25 | return new AccessToken(token, _expiry); 26 | } 27 | 28 | public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 29 | { 30 | var token = await GetBearerToken(); 31 | 32 | return new AccessToken(token, _expiry); 33 | } 34 | 35 | /// 36 | /// Worth revisiting this as a typed client or similar, the wiring is a nightmare. 37 | /// Alternatively we could attempt to patch this in using Aspire events, requires research. 38 | /// 39 | private async ValueTask GetBearerToken() 40 | { 41 | if (!string.IsNullOrEmpty(_token)) 42 | return _token; 43 | 44 | if (string.IsNullOrEmpty(_emulatedVaultUri)) 45 | throw new ArgumentNullException(nameof(_emulatedVaultUri)); 46 | 47 | HttpClient client = null; 48 | 49 | try 50 | { 51 | client = new HttpClient(); 52 | 53 | var response = await client.GetAsync($"{_emulatedVaultUri}/token"); 54 | 55 | response.EnsureSuccessStatusCode(); 56 | 57 | return _token = await response.Content.ReadAsStringAsync(); 58 | } 59 | catch 60 | { 61 | throw; 62 | } 63 | finally 64 | { 65 | client.Dispose(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Models/EmulatedTokenCredential.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Azure.Core; 6 | 7 | namespace AzureKeyVaultEmulator.TestContainers.Models 8 | { 9 | public sealed class EmulatedTokenCredential : TokenCredential 10 | { 11 | public EmulatedTokenCredential(string vaultUri) 12 | { 13 | _emulatedVaultUri = vaultUri; 14 | } 15 | 16 | private string _emulatedVaultUri = string.Empty; 17 | private string _token = string.Empty; 18 | private DateTimeOffset _expiry => DateTimeOffset.Now.AddDays(1); 19 | 20 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 21 | { 22 | // Hate this but someone somewhere will be using Sync methods... 23 | var token = GetBearerToken().GetAwaiter().GetResult(); 24 | 25 | return new AccessToken(token, _expiry); 26 | } 27 | 28 | public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 29 | { 30 | var token = await GetBearerToken(); 31 | 32 | return new AccessToken(token, _expiry); 33 | } 34 | 35 | /// 36 | /// Worth revisiting this as a typed client or similar, the wiring is a nightmare. 37 | /// Alternatively we could attempt to patch this in using Aspire events, requires research. 38 | /// 39 | private async ValueTask GetBearerToken() 40 | { 41 | if (!string.IsNullOrEmpty(_token)) 42 | return _token; 43 | 44 | if (string.IsNullOrEmpty(_emulatedVaultUri)) 45 | throw new ArgumentNullException(nameof(_emulatedVaultUri)); 46 | 47 | HttpClient? client = null; 48 | 49 | try 50 | { 51 | client = new HttpClient(); 52 | 53 | var response = await client.GetAsync($"{_emulatedVaultUri}/token"); 54 | 55 | response.EnsureSuccessStatusCode(); 56 | 57 | return _token = await response.Content.ReadAsStringAsync(); 58 | } 59 | catch 60 | { 61 | throw; 62 | } 63 | finally 64 | { 65 | client?.Dispose(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/RsaPem.cs: -------------------------------------------------------------------------------- 1 | namespace AzureKeyVaultEmulator.Shared.Constants 2 | { 3 | /// 4 | /// Used to encrypt data NOT used by the KeyService. 5 | /// Allows users to persist backups over multiple sessions and still be able to restore them. 6 | /// 7 | public sealed class RsaPem 8 | { 9 | public const string FullPem = @" 10 | -----BEGIN PRIVATE KEY----- 11 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCofdCGPszXrWF7Jxwlhh6m9h56 12 | 9YhbscSK6I9zYkuBBgO0WbyZfZPimGNN5e+LqereEnAUa/ggDaTWldHjmStDz0O9dYr/9v8kWpxJ 13 | QmO6MpzBO/07yuUfCwRjdE8XzbG2BSEgLlVHyTgYDV83h9ThBN5xsiZ0bjqMKS1QrPu75uWBh+WH 14 | QmtDqWO7fx5Ilr/gdCGPBBRsA02/FBZbSRqKYfpFbz7aqtJhUD2QLeqzLq4fQ6JledqU6FDJ6XOP 15 | XEMlaoRaD3/s5odixOao0LlsTq7LWcNU1f2Au4t91lEGuzOLhlPH03sC18N+8Mg1vmOUl7589phG 16 | SFEyxO74sy2dAgMBAAECggEBAJ39O2ZlxJYIEXv09EOLO3q7FWGekbnJOs41uy0qYjoddaPK8TnL 17 | sruqwJLupGuFbKHHEClWBFep84LzANg1a4gt9QrWCPxyklN4U0uuYOzbQHlA0vcaDTXKktbe3Lsp 18 | ORXAQYt3ZqflWh/TihD74PUOJ7bcoYpTQbrjcYZQbcuF9JfX3Tv5spJKmLpHWKyL4XxBZizH92Zp 19 | OqgyuG/VwYyZRfU+9wPNs0pcHkgyFSF4b5n8b3iU6wNGmJJlfu9lOD8ev2L8aZUqdrqJOcCwK4rG 20 | BpS4tRXv4Hk9XTMGjl56xEyHk6juF2iMckLhYmRt6Q7/OfURxa2bLZdiByDOPlECgYEA2RCActeE 21 | CFQSCpNPapq6MsnrDETMP2CABMkwOpiWO2vOZs/Y5JWlIas2AlPx+CGL9WyxSAWtPRJN5FQXRgLS 22 | kcnUw6ZBMaGlcfa5a8Pd9ovAoLuPeCwpxFej+5HneDjWkRQsmRHKjXpWvruOHkhQdbv3T+smTsfd 23 | fgAIzERy69sCgYEAxrbdEYXW9FMAELNoqWo48PfLz1x0ouDRjHD+XgZCfNy1/fHXsFMZjo0UYT5D 24 | /Y1L7xfvYPf8DsVETsuUN8KLGBHR+wRf0zYf9Q663Hvk5XVJNnWBs0LHwgMxGNffC06HqGsA4RW1 25 | eeDN00JP5T3cyNmCN5iDcM4udPBzDlilgecCgYEAtVwxRkLFYUQE8usT7qkqq6bDibOtx8I0FEuY 26 | zUySMUGo6YP93zcdCp2HebhzsnMtAjj3goqjrSQvCngsHeXb082DxJiTXgmGN0sCr4SuXwFzR5iO 27 | jcSwfQkQzO+iK3Op6vulK5uO1liCQ8hnPOwEten//7kkf6xEZrNWpn0GXAMCgYEAqgyEo+En8M8y 28 | aBhPwWKwNa2oENxqx5OiXw+27ZlnvlhVuWoDDNYgMbgTL6BMKKeIyqNt60prvewcJ13ZidoGk+N0 29 | EN5Obn2L3Xbse4/ecmnq7BqkklXcge+fTUY2jgN23a4sA3JDaXfySw4dNuy4inxwDcmK+bbHVLUL 30 | kMRVZhMCgYA2GX8rOCkmlnJWmG3sYD0fP2KzSF5NxAFQq3F39IvwvJ+Inkx2agdZFBynjsg4VJSS 31 | tHfC5zD1uQ8sd/4hCbsK2yQuEsiS+j7Ij15MZvFdJCwxPKV99BTFuxIhnit5xEnqVzb7KNugwDp4 32 | Yu1oryGTUP3I2wF4ta4teRH/4y01Iw== 33 | -----END PRIVATE KEY-----"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Keys/Services/IKeyService.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Shared.Models.Secrets; 2 | 3 | namespace AzureKeyVaultEmulator.Keys.Services 4 | { 5 | public interface IKeyService 6 | { 7 | Task GetKeyAsync(string name); 8 | Task GetKeyAsync(string name, string version); 9 | Task CreateKeyAsync(string name, CreateKey key, bool? managed = null); 10 | Task UpdateKeyAsync(string name, string version, KeyAttributes attributes, Dictionary tags); 11 | Task RotateKey(string name, string version); 12 | 13 | ValueModel GetRandomBytes(int count); 14 | 15 | Task EncryptAsync(string name, string version, KeyOperationParameters keyOperationParameters); 16 | Task DecryptAsync(string keyName, string keyVersion, KeyOperationParameters keyOperationParameters); 17 | 18 | Task?> BackupKeyAsync(string name); 19 | KeyBundle RestoreKey(string jweBody); 20 | 21 | KeyRotationPolicy GetKeyRotationPolicy(string name); 22 | Task UpdateKeyRotationPolicyAsync(string name, KeyRotationAttributes attributes, IEnumerable lifetimeActions); 23 | 24 | ListResult GetKeys(int maxResults = 25, int skipCount = 25); 25 | ListResult GetKeyVersions(string name, int maxResults = 25, int skipCount = 25); 26 | 27 | Task> ReleaseKeyAsync(string name, string version); 28 | Task ImportKeyAsync(string name, JsonWebKey key, KeyAttributes attributes, Dictionary tags); 29 | Task SignWithKeyAsync(string name, string version, string algo, string value); 30 | Task> VerifyDigestAsync(string name, string version, string digest, string signature); 31 | 32 | Task WrapKeyAsync(string name, string version, KeyOperationParameters para); 33 | Task UnwrapKeyAsync(string name, string version, KeyOperationParameters para); 34 | 35 | Task DeleteKeyAsync(string name); 36 | Task GetDeletedKeyAsync(string name); 37 | ListResult GetDeletedKeys(int maxResults = 25, int skipCount = 25); 38 | Task PurgeDeletedKey(string name); 39 | Task RecoverDeletedKeyAsync(string name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Helpers/AzureKeyVaultEnvHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using AzureKeyVaultEmulator.TestContainers.Constants; 6 | using AzureKeyVaultEmulator.TestContainers.Exceptions; 7 | 8 | namespace AzureKeyVaultEmulator.TestContainers.Helpers 9 | { 10 | internal static class AzureKeyVaultEnvHelper 11 | { 12 | private static readonly string[] _defaultVars = new string[] 13 | { 14 | "BUILD_BUILDID", // Azure DevOps 15 | "CI", // Jenkins, TeamCity, etc 16 | "GITHUB_ACTIONS" // Github, obviously. 17 | }; 18 | 19 | public static void Bash(string command) 20 | { 21 | var psi = new ProcessStartInfo 22 | { 23 | FileName = "/bin/bash", 24 | Arguments = $"-c \"{command}\"", 25 | RedirectStandardOutput = true, 26 | RedirectStandardError = true, 27 | UseShellExecute = false, 28 | CreateNoWindow = true 29 | }; 30 | 31 | using var proc = Process.Start(psi); 32 | 33 | var err = proc?.StandardError.ReadToEnd(); 34 | var output = proc?.StandardOutput.ReadToEnd(); 35 | 36 | proc?.WaitForExit(); 37 | 38 | if (!string.IsNullOrEmpty(output)) 39 | Console.WriteLine(output); 40 | 41 | if (!string.IsNullOrEmpty(err)) 42 | Console.WriteLine(err); 43 | 44 | if (proc?.ExitCode != 0) 45 | throw new KeyVaultEmulatorException($"Command failed: {command}\n{err}"); 46 | } 47 | 48 | /// 49 | /// Detects env vars injected by the vast majority of CI/CD runners. 50 | /// 51 | /// 52 | public static bool IsCiCdEnvironment() 53 | => _defaultVars.Any(env => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(env))); 54 | 55 | /// 56 | /// Determines the appropriate container tag based off the current process architecture. 57 | /// linx/arm64/v8 images are built alongside amd64 images, see issue #338 58 | /// 59 | /// The container tag to use for the Emulator. 60 | public static string GetContainerTag() 61 | => RuntimeInformation.ProcessArchitecture == Architecture.Arm64 62 | ? AzureKeyVaultEmulatorContainerConstants.ArmTag 63 | : AzureKeyVaultEmulatorContainerConstants.Tag; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Constants/CertificateContentType.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | using System.Text.RegularExpressions; 3 | using AzureKeyVaultEmulator.Shared.Models.Certificates; 4 | 5 | namespace AzureKeyVaultEmulator.Shared.Constants; 6 | 7 | public static class CertificateContentType 8 | { 9 | private static Regex _typeRegex = new Regex(@"^application\/([^\/\s]+)$", RegexOptions.Compiled); 10 | 11 | // https://pki-tutorial.readthedocs.io/en/latest/mime.html 12 | private static Dictionary _contentTypes = new() 13 | { 14 | { X509ContentType.Unknown, "unknown" }, 15 | 16 | // not relevant, but should be handled 17 | { X509ContentType.SerializedCert, "unknown" }, 18 | { X509ContentType.SerializedStore, "unknown" }, 19 | 20 | // actually used content types from schema above 21 | { X509ContentType.Cert, "pkix-cert" }, 22 | { X509ContentType.Pkcs7, "x-pkcs7-crl" }, 23 | { X509ContentType.Pkcs12, "x-pkcs12" }, 24 | 25 | // no idea, need to generate an Authenticode cert and verify manually. 26 | // Used for code signing, possibly a tiny/obselete use-case for the emulator 27 | { X509ContentType.Authenticode, "x-authenticode" } 28 | }; 29 | 30 | public static string ToApplicationContentType(this X509ContentType contentType) 31 | { 32 | return $"application/{_contentTypes[contentType]}"; 33 | } 34 | 35 | public static X509ContentType FromApplicationContentType(this string? contentType) 36 | { 37 | if (string.IsNullOrWhiteSpace(contentType)) 38 | return X509ContentType.Unknown; 39 | 40 | var matches = _typeRegex.Matches(contentType); 41 | 42 | if(matches.Count == 0) 43 | return X509ContentType.Unknown; 44 | 45 | return _contentTypes.Where(x => x.Value == matches[0].Value).FirstOrDefault().Key; 46 | } 47 | 48 | /// 49 | /// Wrapper to ensure the content type matches an expected type for the response client. 50 | /// 51 | /// The content type provided by the . 52 | /// An application/{contentType} which meets the specs required by the CertificateClient. 53 | public static string ParseCertContentType(this string? contentType) 54 | { 55 | var from = contentType.FromApplicationContentType(); 56 | 57 | return from.ToApplicationContentType(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/publish-all-manual.yml: -------------------------------------------------------------------------------- 1 | name: Publish NuGet Packages and Docker Images 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Build and Test"] 6 | types: 7 | - completed 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | permissions: 16 | id-token: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Fetch all tags 23 | run: git fetch --tags 24 | 25 | - name: Get latest tag 26 | id: get_version 27 | run: | 28 | TAG=$(git tag -l "v*.*.*" | sort -V | tail -n 1) 29 | if [[ -z "$TAG" ]]; then 30 | echo "No Git tag found. Please create a version tag before merging the PR." 31 | exit 1 32 | fi 33 | echo "VERSION=${TAG#v}" >> $GITHUB_ENV 34 | 35 | - name: Setup .NET 36 | uses: actions/setup-dotnet@v3 37 | with: 38 | dotnet-version: | 39 | 8.0.x 40 | 9.0.x 41 | 10.0.x 42 | 43 | - name: Restore dependencies 44 | run: dotnet restore 45 | 46 | - name: Dotnet Build 47 | run: dotnet build --configuration Release --no-restore 48 | 49 | - name: Pack NuGet Package 50 | run: dotnet pack --configuration Release --no-build --output ./nupkgs /p:Version=${{ env.VERSION }} 51 | 52 | - name: Login to NuGet (keyless) 53 | uses: NuGet/login@v1 54 | id: login 55 | with: 56 | user: ${{ secrets.NUGET_USER }} 57 | 58 | - name: Publish to NuGet 59 | run: dotnet nuget push ./nupkgs/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate 60 | 61 | - name: Build Docker Image 62 | run: | 63 | docker build -t jamesgoulddev/azure-keyvault-emulator:latest -t jamesgoulddev/azure-keyvault-emulator:${{ env.VERSION }} . 64 | 65 | - name: Log in to Container Registry 66 | uses: docker/login-action@v3 67 | with: 68 | registry: ${{ secrets.REGISTRY }} 69 | username: ${{ secrets.USERNAME }} 70 | password: ${{ secrets.PASSWORD }} 71 | 72 | - name: Push Latest Docker Image 73 | run: docker push jamesgoulddev/azure-keyvault-emulator:latest 74 | 75 | - name: Push Versioned Docker Image 76 | run: docker push jamesgoulddev/azure-keyvault-emulator:${{ env.VERSION }} -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/AzureKeyVaultEmulator.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net10.0 4 | enable 5 | enable 6 | false 7 | true 8 | 1f5e0e4e-442f-475f-8662-94a562031d22 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Aspire.Hosting/Helpers/AzureKeyVaultEnvHelper.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Aspire.Hosting.Exceptions; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace AzureKeyVaultEmulator.Aspire.Hosting.Helpers; 6 | 7 | internal static class AzureKeyVaultEnvHelper 8 | { 9 | private static readonly string[] _defaultVars = 10 | [ 11 | "BUILD_BUILDID", // Azure DevOps 12 | "CI", // Jenkins, TeamCity, etc 13 | "GITHUB_ACTIONS" // Github, obviously. 14 | ]; 15 | 16 | /// 17 | /// Typically used to run commands with `sudo` 18 | /// 19 | /// 20 | public static void Bash(string command) 21 | { 22 | Exec("/bin/bash", $"-c \"{command}\""); 23 | } 24 | 25 | public static void Exec(string executable, string arguments) 26 | { 27 | var psi = new ProcessStartInfo 28 | { 29 | FileName = executable, 30 | Arguments = arguments, 31 | RedirectStandardOutput = true, 32 | RedirectStandardError = true, 33 | UseShellExecute = false, 34 | CreateNoWindow = true 35 | }; 36 | 37 | using var proc = Process.Start(psi); 38 | 39 | var err = proc?.StandardError.ReadToEnd(); 40 | var output = proc?.StandardOutput.ReadToEnd(); 41 | 42 | proc?.WaitForExit(); 43 | 44 | if (!string.IsNullOrEmpty(output)) 45 | Console.WriteLine(output); 46 | 47 | if (!string.IsNullOrEmpty(err)) 48 | Console.WriteLine(err); 49 | 50 | if (proc?.ExitCode != 0) 51 | throw new KeyVaultEmulatorException($"Command failed: {executable} {arguments}\n{err}"); 52 | } 53 | 54 | /// 55 | /// Detects env vars injected by the vast majority of CI/CD runners. 56 | /// 57 | /// 58 | public static bool IsCiCdEnvironment() 59 | => _defaultVars.Any(env => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(env))); 60 | 61 | /// 62 | /// Determines the appropriate container tag based off the current process architecture. 63 | /// linx/arm64/v8 images are built alongside amd64 images, see issue #338 64 | /// 65 | /// The container tag to use for the Emulator. 66 | public static string GetContainerTag() 67 | => RuntimeInformation.ProcessArchitecture == Architecture.Arm64 68 | ? KeyVaultEmulatorContainerConstants.ArmTag 69 | : KeyVaultEmulatorContainerConstants.Tag; 70 | } 71 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Utilities/HttpRequestUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace AzureKeyVaultEmulator.Shared.Utilities 4 | { 5 | public static class HttpRequestUtils 6 | { 7 | private const string _apiVersion = "api-version"; 8 | /// 9 | /// Provides the nextLink property when retrieving an from a vault.
10 | /// Used when a MaxCount optional parameter is provided. 11 | ///
12 | public static string GetNextLink(this IHttpContextAccessor context, string skipToken, int max = 25) 13 | { 14 | ArgumentNullException.ThrowIfNull(context.HttpContext); 15 | 16 | var http = context.HttpContext; 17 | 18 | var exists = http.Request.Query.TryGetValue(_apiVersion, out var version); 19 | 20 | if (!exists) 21 | throw new InvalidOperationException($"Could not parse api-version header when generated nextLink"); 22 | 23 | var builder = new Uri($"{http.Request.Scheme}://{http.Request.Host}{http.Request.Path}"); 24 | 25 | var queryParam = $"?{_apiVersion}={version}&$skipToken={skipToken}&maxresults={max}"; 26 | 27 | return $"{builder.AbsoluteUri}{queryParam}"; 28 | } 29 | 30 | /// 31 | /// Constructs the URI that's used to create KeyIdentifier, SecretIdentifier or CertificateIdentifier. 32 | /// 33 | /// The from the request. 34 | /// The name of the item to create an identifier for. 35 | /// The path of the request, typically the name of the item being created (keys/secrets/certificates). 36 | /// Optional version. 37 | /// A fully compliant 38 | public static string BuildIdentifierUri(this IHttpContextAccessor context, string name, string version, string path) 39 | { 40 | ArgumentException.ThrowIfNullOrWhiteSpace(name); 41 | ArgumentException.ThrowIfNullOrWhiteSpace(path); 42 | ArgumentException.ThrowIfNullOrWhiteSpace(version); 43 | 44 | var builder = new UriBuilder 45 | { 46 | Scheme = context.HttpContext?.Request.Scheme, 47 | Host = context.HttpContext?.Request.Host.Host, 48 | Port = context.HttpContext?.Request.Host.Port ?? -1, 49 | Path = $"{path}/{name}/{version}" 50 | }; 51 | 52 | return builder.Uri.ToString(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Client/KeyVaultHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Azure.Security.KeyVault.Certificates; 3 | using Azure.Security.KeyVault.Keys; 4 | using Azure.Security.KeyVault.Secrets; 5 | 6 | namespace AzureKeyVaultEmulator.Aspire.Client 7 | { 8 | /// 9 | /// Helper class for creating Azure KeyVault clients configured for the emulator. 10 | /// 11 | public static class KeyVaultHelper 12 | { 13 | /// 14 | /// Gets a configured for the Azure KeyVault Emulator. 15 | /// 16 | /// The emulator endpoint URL. 17 | /// A configured . 18 | public static SecretClient GetSecretClient(string vaultEndpoint) 19 | { 20 | if (string.IsNullOrEmpty(vaultEndpoint)) 21 | throw new ArgumentNullException(nameof(vaultEndpoint)); 22 | 23 | var credential = new EmulatedTokenCredential(vaultEndpoint); 24 | var uri = new Uri(vaultEndpoint); 25 | 26 | return new SecretClient(uri, credential, new SecretClientOptions { DisableChallengeResourceVerification = true }); 27 | } 28 | 29 | /// 30 | /// Gets a configured for the Azure KeyVault Emulator. 31 | /// 32 | /// The emulator endpoint URL. 33 | /// A configured . 34 | public static KeyClient GetKeyClient(string vaultEndpoint) 35 | { 36 | if (string.IsNullOrEmpty(vaultEndpoint)) 37 | throw new ArgumentNullException(nameof(vaultEndpoint)); 38 | 39 | var credential = new EmulatedTokenCredential(vaultEndpoint); 40 | var uri = new Uri(vaultEndpoint); 41 | 42 | return new KeyClient(uri, credential, new KeyClientOptions { DisableChallengeResourceVerification = true }); 43 | } 44 | 45 | /// 46 | /// Gets a configured for the Azure KeyVault Emulator. 47 | /// 48 | /// The emulator endpoint URL. 49 | /// A configured . 50 | public static CertificateClient GetCertificateClient(string vaultEndpoint) 51 | { 52 | if (string.IsNullOrEmpty(vaultEndpoint)) 53 | throw new ArgumentNullException(nameof(vaultEndpoint)); 54 | 55 | var credential = new EmulatedTokenCredential(vaultEndpoint); 56 | var uri = new Uri(vaultEndpoint); 57 | 58 | return new CertificateClient(uri, credential, new CertificateClientOptions { DisableChallengeResourceVerification = true }); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Client/AddEmulatorSupport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Azure.Security.KeyVault.Certificates; 3 | using Azure.Security.KeyVault.Keys; 4 | using Azure.Security.KeyVault.Secrets; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace AzureKeyVaultEmulator.Aspire.Client 9 | { 10 | public static class AddEmulatorSupport 11 | { 12 | /// 13 | /// Creates the scaffolding for AzureKeyVault support using the containerised emulator. 14 | /// 15 | /// The to inject into. 16 | /// The endpoint from the for the containerised AzureKeyVaultEmulator.
Typically found in 17 | /// Bool to create a , defaults to 18 | /// Bool to create a , defaults to 19 | /// Bool to create a , defaults to 20 | /// 21 | /// Thrown if you attempt to use the Emulator outside of a DEBUG environment. 22 | /// 23 | /// 24 | /// Thrown if you do not provide the BaseUrl for the KeyVaultEmulator container 25 | /// An updated 26 | public static IServiceCollection AddAzureKeyVaultEmulator( 27 | this IServiceCollection services, 28 | string vaultEndpoint, 29 | bool secrets = true, 30 | bool keys = false, 31 | bool certificates = false) 32 | { 33 | if (string.IsNullOrEmpty(vaultEndpoint)) 34 | throw new ArgumentNullException(vaultEndpoint); 35 | 36 | var credential = new EmulatedTokenCredential(vaultEndpoint); 37 | var uri = new Uri(vaultEndpoint); 38 | 39 | if (secrets) 40 | services.AddTransient(x => 41 | new SecretClient(uri, credential, new SecretClientOptions { DisableChallengeResourceVerification = true })); 42 | 43 | if (keys) 44 | services.AddTransient(x => 45 | new KeyClient(uri, credential, new KeyClientOptions { DisableChallengeResourceVerification = true })); 46 | 47 | if (certificates) 48 | services.AddTransient(x => 49 | new CertificateClient(uri, credential, new CertificateClientOptions { DisableChallengeResourceVerification = true })); 50 | 51 | return services; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Secrets/Controllers/DeletedSecretsController.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Secrets.Services; 2 | using AzureKeyVaultEmulator.Shared.Models.Secrets; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace AzureKeyVaultEmulator.Secrets.Controllers 7 | { 8 | [ApiController] 9 | [Route("deletedsecrets")] 10 | [Authorize] 11 | public class DeletedSecretsController(ISecretService secretService, ITokenService tokenService) : Controller 12 | { 13 | [HttpGet("{name}")] 14 | [Produces("application/json")] 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 17 | public async Task GetDeletedSecret( 18 | [FromRoute] string name, 19 | [ApiVersion] string apiVersion) 20 | { 21 | var bundle = await secretService.GetDeletedSecretAsync(name); 22 | 23 | return Ok(bundle); 24 | } 25 | 26 | [HttpGet] 27 | [Produces("application/json")] 28 | [ProducesResponseType>(StatusCodes.Status200OK)] 29 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 30 | public IActionResult GetDeletedSecrets( 31 | [ApiVersion] string apiVersion, 32 | [FromQuery] int maxResults = 25, 33 | [SkipToken] string skipToken = "") 34 | { 35 | var skipCount = 0; 36 | 37 | if (!string.IsNullOrEmpty(skipToken)) 38 | skipCount = tokenService.DecodeSkipToken(skipToken); 39 | 40 | var deletedSecrets = secretService.GetDeletedSecrets(maxResults, skipCount); 41 | 42 | return Ok(deletedSecrets); 43 | } 44 | 45 | [HttpDelete("{name}")] 46 | [Produces("application/json")] 47 | [ProducesResponseType(StatusCodes.Status204NoContent)] 48 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 49 | public async Task PurgeDeletedSecret( 50 | [FromRoute] string name, 51 | [ApiVersion] string apiVersion) 52 | { 53 | await secretService.PurgeDeletedSecretAsync(name); 54 | 55 | return NoContent(); 56 | } 57 | 58 | [HttpPost("{name}/recover")] 59 | [Produces("application/json")] 60 | [ProducesResponseType>(StatusCodes.Status200OK)] 61 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 62 | public async Task RecoverDeletedSecret( 63 | [FromRoute] string name, 64 | [ApiVersion] string apiVersion) 65 | { 66 | var secret = await secretService.RecoverDeletedSecretAsync(name); 67 | 68 | return Ok(secret); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Certificates/Controllers/DeletedCertificatesController.cs: -------------------------------------------------------------------------------- 1 | using AzureKeyVaultEmulator.Certificates.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace AzureKeyVaultEmulator.Certificates.Controllers; 6 | 7 | [Route("deletedcertificates")] 8 | [ApiController] 9 | [Authorize] 10 | public class DeletedCertificatesController( 11 | ICertificateService certService, 12 | ITokenService tokenService) : Controller 13 | { 14 | [HttpGet] 15 | public async Task GetDeletedCertificates( 16 | [ApiVersion] string apiVersion, 17 | [FromQuery] bool includePending = true, 18 | [FromQuery] int maxResults = 25, 19 | [SkipToken] string skipToken = "") 20 | { 21 | var skipCount = 0; 22 | 23 | if (!string.IsNullOrEmpty(skipToken)) 24 | skipCount = tokenService.DecodeSkipToken(skipToken); 25 | 26 | var result = await certService.GetDeletedCertificatesAsync(maxResults, skipCount); 27 | 28 | return Ok(result); 29 | } 30 | 31 | [HttpGet("{name}")] 32 | public async Task GetDeletedCertificate( 33 | [FromRoute] string name, 34 | [ApiVersion] string apiVersion) 35 | { 36 | ArgumentException.ThrowIfNullOrEmpty(name); 37 | 38 | var result = await certService.GetDeletedCertificateAsync(name); 39 | 40 | return Accepted(result); 41 | } 42 | 43 | [HttpGet("{name}/pending")] 44 | public async Task GetPendingDeletedCertificate( 45 | [FromRoute] string name, 46 | [ApiVersion] string apiVersion) 47 | { 48 | ArgumentException.ThrowIfNullOrEmpty(name); 49 | 50 | var result = await certService.GetPendingDeletedCertificateAsync(name); 51 | 52 | return Ok(result); 53 | } 54 | 55 | [HttpPost("{name}/recover")] 56 | public async Task StartRecoveringCertificate( 57 | [FromRoute] string name, 58 | [ApiVersion] string apiVersion) 59 | { 60 | ArgumentException.ThrowIfNullOrEmpty(name); 61 | 62 | var result = await certService.RecoverCerticateAsync(name); 63 | 64 | return Ok(result); 65 | } 66 | 67 | [HttpPost("{name}/recover/pending")] 68 | public async Task GetPendingRecoveringCertificate( 69 | [FromRoute] string name, 70 | [ApiVersion] string apiVersion) 71 | { 72 | ArgumentException.ThrowIfNullOrEmpty(name); 73 | 74 | var result = await certService.GetPendingDeletedCertificateAsync(name); 75 | 76 | return Ok(result); 77 | } 78 | 79 | [HttpDelete("{name}")] 80 | public async Task PurgeCertificate( 81 | [FromRoute] string name, 82 | [ApiVersion] string apiVersion) 83 | { 84 | ArgumentException.ThrowIfNullOrEmpty(name); 85 | 86 | await certService.PurgeDeletedCertificateAsync(name); 87 | 88 | return NoContent(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/X509CertificateProperties.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Text.Json.Serialization; 4 | using Azure.Security.KeyVault.Certificates; 5 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 6 | using System.Text.Json; 7 | 8 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 9 | 10 | public sealed class X509CertificateProperties : IPersistedItem 11 | { 12 | [Key] 13 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 14 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 15 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 16 | 17 | public string BackingEnhancedUsage { get; set; } = "[]"; 18 | 19 | [JsonPropertyName("ekus")] 20 | [NotMapped] 21 | public IEnumerable EnhancedKeyUsage 22 | { 23 | get => JsonSerializer.Deserialize>(BackingEnhancedUsage) ?? []; 24 | set => BackingEnhancedUsage = JsonSerializer.Serialize(value); 25 | } 26 | 27 | [JsonPropertyName("key_usage")] 28 | public IEnumerable BackingKeyUsage { get; set; } = []; 29 | 30 | [JsonIgnore] 31 | [NotMapped] 32 | public IEnumerable KeyUsage 33 | { 34 | get => BackingKeyUsage.Select(keyUsage => new CertificateKeyUsage(keyUsage)); 35 | set => BackingKeyUsage = value.Select(keyUsage => keyUsage.ToString()); 36 | } 37 | 38 | [JsonPropertyName("sans")] 39 | public SubjectAlternativeNames SubjectAlternativeNames { get; set; } = new(); 40 | 41 | [JsonPropertyName("subject")] 42 | public string Subject { get; set; } = string.Empty; 43 | 44 | [JsonPropertyName("validity_months")] 45 | public int ValidityMonths { get; set; } = 0; 46 | } 47 | 48 | public sealed class SubjectAlternativeNames : IPersistedItem 49 | { 50 | [Key] 51 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 52 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 53 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 54 | 55 | public string BackingDns { get; set; } = "[]"; 56 | 57 | [JsonPropertyName("dns_names")] 58 | [NotMapped] 59 | public IEnumerable DnsNames 60 | { 61 | get => JsonSerializer.Deserialize>(BackingDns) ?? []; 62 | set => BackingDns = JsonSerializer.Serialize(value); 63 | } 64 | 65 | public string BackingEmails { get; set; } = "[]"; 66 | 67 | [JsonPropertyName("emails")] 68 | [NotMapped] 69 | public IEnumerable Emails 70 | { 71 | get => JsonSerializer.Deserialize>(BackingEmails) ?? []; 72 | set => BackingEmails = JsonSerializer.Serialize(value); 73 | } 74 | 75 | public string BackingUpns { get; set; } = "[]"; 76 | 77 | [JsonPropertyName("upns")] 78 | [NotMapped] 79 | public IEnumerable Upns 80 | { 81 | get => JsonSerializer.Deserialize>(BackingUpns) ?? []; 82 | set => BackingUpns = JsonSerializer.Serialize(value); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/CertificateBundle.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Security.Cryptography.X509Certificates; 4 | using System.Text.Json.Serialization; 5 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 6 | 7 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 8 | 9 | public sealed class CertificateBundle 10 | : CertificateProperties, INamedItem, IAttributedModel 11 | { 12 | [Key] 13 | [JsonIgnore] 14 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 15 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 16 | 17 | public string PersistedName { get; set; } = string.Empty; 18 | 19 | public string PersistedVersion { get; set; } = string.Empty; 20 | 21 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 22 | public bool Deleted { get; set; } = false; 23 | 24 | [JsonPropertyName("policy")] 25 | public CertificatePolicy? CertificatePolicy { get; set; } 26 | 27 | [JsonPropertyName("cer")] 28 | public string CertificateContents { get; set; } = string.Empty; 29 | 30 | [JsonPropertyName("kid")] 31 | public string KeyId { get; set; } = string.Empty; 32 | 33 | [JsonPropertyName("sid")] 34 | public string SecretId { get; set; } = string.Empty; 35 | 36 | public byte[] CertificateBlob { get; set; } = []; 37 | 38 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 39 | [NotMapped] 40 | private X509Certificate2? _certificate; 41 | 42 | /// 43 | /// Here to facilitate testing, we need the RSA private key available. 44 | /// Cer is only the public key information, only other option is we hardcode an export or have files on disk. 45 | /// 46 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 47 | [NotMapped] 48 | public X509Certificate2? FullCertificate 49 | { 50 | get 51 | { 52 | if(_certificate != null) 53 | return _certificate; 54 | 55 | return CertificateBlob is null || CertificateBlob.Length == 0 56 | ? null 57 | : _certificate = CertificateBlobSerializer.Deserialize(CertificateBlob, "emulator"); 58 | } 59 | set 60 | { 61 | CertificateBlob = 62 | value is null ? [] : CertificateBlobSerializer.Serialize(value, "emulator"); 63 | 64 | } 65 | } 66 | } 67 | 68 | public static class CertificateBundleCloning 69 | { 70 | public static CertificateBundle CopyWithNewCertificate(this CertificateBundle bundle, X509Certificate2 newCertificate) 71 | { 72 | return new() 73 | { 74 | CertificateIdentifier = bundle.CertificateIdentifier, 75 | RecoveryId = bundle.RecoveryId, 76 | CertificatePolicy = bundle.CertificatePolicy, 77 | CertificateContents = Convert.ToBase64String(newCertificate.RawData), 78 | KeyId = bundle.KeyId, 79 | SecretId = bundle.SecretId, 80 | Attributes = bundle.Attributes, 81 | CertificateName = bundle.CertificateName, 82 | VaultUri = bundle.VaultUri, 83 | X509Thumbprint = newCertificate.Thumbprint, 84 | Tags = bundle.Tags 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Client/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This library simplifies the inclusion of the [Azure Key Vault Emulator](https://github.com/james-gould/azure-keyvault-emulator) into your local development environment. 4 | 5 | You do not *need* to use it but it makes life easier. Due to the constraints of Azure Key Vault and the associated client libraries, any requests that don't come from `https://*.vault.azure.net` are rejected. 6 | 7 | To work around this you need to set `DisableChallengeResourceVerification = true` for each client. This library does that for you, emulates the authentication and then dependency injects the clients you need. 8 | 9 | # Setup 10 | 11 | First install the package to your application that needs to use Key Vault: 12 | 13 | ``` 14 | dotnet add package AzureKeyVaultEmulator.Client 15 | ``` 16 | 17 | Next you'll need to reference the `vaultUri` which points at the docker container. 18 | 19 | If you're using `.NET Aspire` this will appear in your `appsettings.json`: 20 | 21 | ```json 22 | { 23 | "Logging": { 24 | "LogLevel": { 25 | "Default": "Information", 26 | "Microsoft.AspNetCore": "Warning" 27 | } 28 | }, 29 | "AllowedHosts": "*", 30 | "ConnectionStrings": { 31 | "myAspireResourceName": "" 32 | } 33 | } 34 | 35 | ``` 36 | 37 | > [!NOTE] 38 | > You don't need to add this into your `appsettings.json` beforehand, Aspire will do this for you. 39 | 40 | If you're using the Emulator image directly through `Docker` your vault URI will be `https://localhost:4997`. 41 | 42 | # Usage 43 | 44 | Including support for the emulator is simple: 45 | 46 | ```csharp 47 | // Injected by Aspire using the name "keyvault". 48 | var vaultUri = builder.Configuration.GetConnectionString("keyvault") ?? string.Empty; 49 | 50 | // Basic Secrets only implementation 51 | builder.Services.AddAzureKeyVaultEmulator(vaultUri); 52 | ``` 53 | 54 | You can configure which `Clients` you want to expose like so: 55 | 56 | ```csharp 57 | builder.Services.AddAzureKeyVaultEmulator(vaultUri, secrets: true, keys: true, certificates: false); 58 | ``` 59 | 60 | By default only a `SecretClient` will be available, but you can easily add `CertificateClient` and `KeyClient` support. 61 | 62 | # Access 63 | 64 | Now you've got your clients set up you can simply use them like you would any other DI service: 65 | 66 | ```csharp 67 | private SecretClient _secretClient; 68 | 69 | public SecretsController(SecretClient secretClient) 70 | { 71 | _secretClient = secretClient; 72 | } 73 | 74 | public async Task GetSecret(string name) 75 | { 76 | var secret = await _secretClient.GetSecretAsync(name); 77 | } 78 | ``` 79 | 80 | # Quick Tip 81 | 82 | To make life easy you can create an environment flag in your `Program.cs` to use the Emulator in a dev environment and the hosted Vault in production: 83 | 84 | ```csharp 85 | var vaultUri = builder.Configuration.GetConnectionString("keyvault") ?? string.Empty; 86 | 87 | if(builder.Environment.IsDevelopment()) 88 | builder.Services.AddAzureKeyVaultEmulator(vaultUri, secrets: true, certificates: true, keys: true); 89 | else 90 | builder.Services.AddAzureClients(client => 91 | { 92 | var asUri = new Uri(vaultUri); 93 | 94 | client.AddSecretClient(asUri); 95 | client.AddKeyClient(asUri); 96 | client.AddCertificateClient(asUri); 97 | }); 98 | ``` 99 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator/Emulator/Services/TokenService.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | 4 | namespace AzureKeyVaultEmulator.Emulator.Services 5 | { 6 | public interface ITokenService 7 | { 8 | string CreateBearerToken(IEnumerable? inboundClaims = null); 9 | string CreateSkipToken(int skipCount); 10 | int DecodeSkipToken(string skipToken); 11 | string CreateTokenWithHeaderClaim(IEnumerable payloadClaims, string headerClaimType, string headerClaimValue); 12 | } 13 | 14 | public class TokenService : ITokenService 15 | { 16 | private const string _skipClaim = "skipCount"; 17 | 18 | public string CreateBearerToken(IEnumerable? inboundClaims = null) 19 | { 20 | if (inboundClaims is null) 21 | inboundClaims = []; 22 | 23 | var claims = new[] 24 | { 25 | new Claim(JwtRegisteredClaimNames.Sub, "localuser"), 26 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) 27 | }; 28 | 29 | return CreateToken([.. inboundClaims, .. claims]); 30 | } 31 | 32 | public string CreateSkipToken(int skipCount) 33 | { 34 | var claim = new Claim(_skipClaim, $"{skipCount}"); 35 | 36 | return CreateToken([claim]); 37 | } 38 | 39 | public int DecodeSkipToken(string skipToken) 40 | { 41 | var token = new JwtSecurityToken(skipToken); 42 | 43 | var skipClaim = token.Claims.FirstOrDefault(x => x.Type.Equals(_skipClaim, StringComparison.OrdinalIgnoreCase)); 44 | 45 | if (skipClaim is null) 46 | return default; 47 | 48 | var validSkipClaim = int.TryParse(skipClaim.Value, out int skipCount); 49 | 50 | return validSkipClaim ? skipCount : default; 51 | } 52 | 53 | public string CreateTokenWithHeaderClaim( 54 | IEnumerable payloadClaims, 55 | string headerClaimType, 56 | string headerClaimValue) 57 | { 58 | var key = AuthConstants.SigningKey; 59 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 60 | 61 | var token = new JwtSecurityToken( 62 | issuer: AuthConstants.EmulatorIss, 63 | audience: AuthConstants.EmulatorIss, 64 | claims: [.. payloadClaims], 65 | expires: DateTime.Now.AddMinutes(30), 66 | signingCredentials: creds); 67 | 68 | token.Header.Add(headerClaimType, headerClaimValue); 69 | 70 | return new JwtSecurityTokenHandler().WriteToken(token); 71 | } 72 | 73 | private static string CreateToken(IEnumerable claims) 74 | { 75 | var key = AuthConstants.SigningKey; 76 | 77 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 78 | 79 | var token = new JwtSecurityToken( 80 | issuer: "localazurekeyvault.localhost.com", 81 | audience: "localazurekeyvault.localhost.com", 82 | claims: [.. claims], 83 | expires: DateTime.Now.AddMinutes(30), 84 | signingCredentials: creds); 85 | 86 | return new JwtSecurityTokenHandler().WriteToken(token); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TestContainers/dotnet/Helpers/AzureKeyVaultEmulatorClientHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Azure.Security.KeyVault.Certificates; 3 | using Azure.Security.KeyVault.Keys; 4 | using Azure.Security.KeyVault.Secrets; 5 | using AzureKeyVaultEmulator.TestContainers.Models; 6 | 7 | namespace AzureKeyVaultEmulator.TestContainers.Helpers 8 | { 9 | 10 | public static class AzureKeyVaultEmulatorClientHelper 11 | { 12 | /// 13 | /// Gets a configured for the Azure KeyVault Emulator. 14 | /// 15 | /// The TestContainers container hosting the AzureKeyVaultEmulator image. 16 | /// A configured . 17 | public static SecretClient GetSecretClient(this AzureKeyVaultEmulatorContainer container) 18 | { 19 | if (container == null) 20 | throw new ArgumentNullException(nameof(container)); 21 | 22 | var vaultEndpoint = container.GetConnectionString(); 23 | 24 | if (string.IsNullOrEmpty(vaultEndpoint)) 25 | throw new ArgumentNullException(nameof(vaultEndpoint)); 26 | 27 | var credential = new EmulatedTokenCredential(vaultEndpoint); 28 | var uri = new Uri(vaultEndpoint); 29 | 30 | return new SecretClient(uri, credential, new SecretClientOptions { DisableChallengeResourceVerification = true }); 31 | } 32 | 33 | /// 34 | /// Gets a configured for the Azure KeyVault Emulator. 35 | /// 36 | /// The TestContainers container hosting the AzureKeyVaultEmulator image. 37 | /// A configured . 38 | public static KeyClient GetKeyClient(this AzureKeyVaultEmulatorContainer container) 39 | { 40 | if (container == null) 41 | throw new ArgumentNullException(nameof(container)); 42 | 43 | var vaultEndpoint = container.GetConnectionString(); 44 | 45 | if (string.IsNullOrEmpty(vaultEndpoint)) 46 | throw new ArgumentNullException(nameof(vaultEndpoint)); 47 | 48 | var credential = new EmulatedTokenCredential(vaultEndpoint); 49 | var uri = new Uri(vaultEndpoint); 50 | 51 | return new KeyClient(uri, credential, new KeyClientOptions { DisableChallengeResourceVerification = true }); 52 | } 53 | 54 | /// 55 | /// Gets a configured for the Azure KeyVault Emulator. 56 | /// 57 | /// /// The TestContainers container hosting the AzureKeyVaultEmulator image. 58 | /// A configured . 59 | public static CertificateClient GetCertificateClient(this AzureKeyVaultEmulatorContainer container) 60 | { 61 | if (container == null) 62 | throw new ArgumentNullException(nameof(container)); 63 | 64 | var vaultEndpoint = container.GetConnectionString(); 65 | 66 | if (string.IsNullOrEmpty(vaultEndpoint)) 67 | throw new ArgumentNullException(nameof(vaultEndpoint)); 68 | 69 | var credential = new EmulatedTokenCredential(vaultEndpoint); 70 | var uri = new Uri(vaultEndpoint); 71 | 72 | return new CertificateClient(uri, credential, new CertificateClientOptions { DisableChallengeResourceVerification = true }); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/CertificateUtilities/README.md: -------------------------------------------------------------------------------- 1 | # Certificate Utilities for the Azure Key Vault Emulator 2 | 3 | > [!NOTE] 4 | > You will only have to do this once and it should take around 3 minutes. 5 | 6 | The Azure Key Vault Client SDK requires `HTTPS` and trusted SSL for valid connections - breaking either of these conditions will cause all requests to fail at runtime. 7 | 8 | The `AzureKeyVaultEmulator.Aspire.Hosting` library will generate and install these certificates for you. If you're not using Aspire, or wish to provide your own certificates, follow the instructions below to get started using the Emulator. 9 | 10 | # Automated Certificate Creation 11 | 12 | The setup process can be fully automated by using the installation script: 13 | 14 | ``` 15 | bash <(curl -fsSL https://raw.githubusercontent.com/james-gould/azure-keyvault-emulator/refs/heads/master/docs/setup.sh) 16 | ``` 17 | 18 | > [!IMPORTANT] 19 | > If you're using **Windows**, use `Git Bash` or `wsl -u root` to execute the setup script. 20 | 21 | ## Creating valid certificates 22 | 23 | ### With dotnet 24 | 25 | If you have `.NET` installed you will have access to the CLI command `dotnet dev-certs` which creates valid, `localhost` SSL certificates. 26 | 27 | - Windows: [dotnet/create-certs.ps1](dotnet/create-certs.ps1) 28 | - Linux/Mac: [dotnet/create-certs.sh](dotnet/create-certs.sh) 29 | 30 | If you don't want to run the script, and would rather execute the commands yourself, simply copy the (very brief) commands from the files into your local terminal. 31 | 32 | ### Without dotnet 33 | 34 | You can generate self-signed SSL certificates with `openssl`, a free utility that handily comes packaged with `git`. 35 | 36 | If you're on Windows and `openssl` isn't on your `PATH`: 37 | 38 | - Navigate to `C:\Program Files\Git\usr\bin` and verify that `openssl.exe` is present. 39 | - Your local install directory may be different, but the `openssl.exe` will be under `usr\bin\` 40 | - Edit your environment variables and add the folder path to your `PATH` variable. 41 | - Restart your terminal. 42 | 43 | If you're on Linux/MacOS and `openssl` isn't available simply run: 44 | 45 | `sudo apt-get install libssl-dev`. 46 | 47 | With `openssl` available on your command line, create `emulator.crt` you can now run [openssl/create-certs.sh](openssl/create-certs.sh). 48 | 49 | If you don't want to run the script, and would rather execute the commands yourself, simply copy the (very brief) commands from the files into your local terminal. 50 | 51 | ### Installation 52 | 53 | Now you must install a certificate as a **Trusted Root CA**: 54 | 55 | - On Windows install `emulator.pfx`. 56 | - Right click -> Install and follow the installation wizard. 57 | - Otherwise install `emulator.crt`. 58 | - `Linux`: Run `cp emulator.crt /usr/local/share/ca-certificates/emulator.crt && update-ca-certificates` 59 | - MacOS: Run `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ` 60 | 61 | ### Known limitations 62 | 63 | A limitation of the Emulator is the inability to define the name and password of the SSL certificates. This is being investigated but currently they must **both** be `emulator`, ie you must generate `emulator.pfx` with the password `emulator` and `emulator.crt`. 64 | 65 | ## Configure the Emulator to use your certificates 66 | 67 | You can now follow the [local system configuration](../CONFIG.md) to manually set these up for the Emulator. 68 | 69 | Don't worry, it's a very quick process. 70 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/Keys/DeletedKeysControllerTests.cs: -------------------------------------------------------------------------------- 1 | using Azure.Security.KeyVault.Keys; 2 | using AzureKeyVaultEmulator.IntegrationTests.SetupHelper; 3 | using AzureKeyVaultEmulator.IntegrationTests.SetupHelper.Fixtures; 4 | 5 | namespace AzureKeyVaultEmulator.IntegrationTests.Keys; 6 | 7 | public sealed class DeletedKeysControllerTests(KeysTestingFixture fixture) : IClassFixture 8 | { 9 | [Fact] 10 | public async Task GetDeletedKeyReturnsFromDeletedKeyStore() 11 | { 12 | var client = await fixture.GetClientAsync(); 13 | 14 | var keyName = fixture.FreshlyGeneratedGuid; 15 | 16 | var createdKey = await fixture.CreateKeyAsync(keyName); 17 | 18 | var deletedKey = (await client.StartDeleteKeyAsync(keyName)).Value; 19 | 20 | await Assert.RequestFailsAsync(() => client.GetKeyAsync(keyName)); 21 | 22 | var fromDeletedStore = await client.GetDeletedKeyAsync(keyName); 23 | 24 | Assert.KeysAreEqual(createdKey, deletedKey); 25 | } 26 | 27 | [Fact] 28 | public async Task GetDeletedKeysWillCycleLink() 29 | { 30 | var client = await fixture.GetClientAsync(); 31 | 32 | var keyName = fixture.FreshlyGeneratedGuid; 33 | 34 | var executionCount = await 35 | RequestSetup.CreateMultiple(26, 30, y => client.CreateKeyAsync(keyName, KeyType.Rsa)); 36 | 37 | var deleteOperation = (await client.StartDeleteKeyAsync(keyName)).Value; 38 | 39 | List detectedDeletedKeys = []; 40 | 41 | await foreach (var deletedKey in client.GetDeletedKeysAsync()) 42 | if (deletedKey?.Name?.Equals(keyName, StringComparison.OrdinalIgnoreCase) == true) 43 | detectedDeletedKeys.Add(deletedKey); 44 | 45 | Assert.Equal(executionCount, detectedDeletedKeys.Count); 46 | } 47 | 48 | [Fact] 49 | public async Task PurgeDeletedKeyRemovedFromDeletedStore() 50 | { 51 | var client = await fixture.GetClientAsync(); 52 | 53 | var keyName = fixture.FreshlyGeneratedGuid; 54 | 55 | var createdKey = await fixture.CreateKeyAsync(keyName); 56 | 57 | var deleteOperation = await client.StartDeleteKeyAsync(keyName); 58 | 59 | var deletedKey = await client.GetDeletedKeyAsync(keyName); 60 | 61 | Assert.KeysAreEqual(createdKey, deletedKey); 62 | 63 | var purgeResult = await client.PurgeDeletedKeyAsync(keyName); 64 | 65 | await Assert.RequestFailsAsync(() => client.GetDeletedKeyAsync(keyName)); 66 | 67 | await Assert.RequestFailsAsync(() => client.GetKeyAsync(keyName)); 68 | } 69 | 70 | [Fact] 71 | public async Task RestoreKeyRemovesFromDeletedStore() 72 | { 73 | var client = await fixture.GetClientAsync(); 74 | 75 | var keyName = fixture.FreshlyGeneratedGuid; 76 | 77 | var createdKey = await fixture.CreateKeyAsync(keyName); 78 | 79 | await client.StartDeleteKeyAsync(keyName); 80 | 81 | var deletedKey = (await client.GetDeletedKeyAsync(keyName)).Value; 82 | 83 | Assert.KeysAreEqual(deletedKey, createdKey); 84 | 85 | var restoredKey = (await client.StartRecoverDeletedKeyAsync(keyName)).Value; 86 | 87 | Assert.KeysAreEqual(createdKey, restoredKey); 88 | 89 | await Assert.RequestFailsAsync(() => client.GetDeletedKeyAsync(keyName)); 90 | 91 | var fromMainStore = (await client.GetKeyAsync(keyName)).Value; 92 | 93 | Assert.KeysAreEqual(createdKey, fromMainStore); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/AzureKeyVaultEmulator.IntegrationTests/Certificates/Helpers/X509Certificate With PEM Generation.linq: -------------------------------------------------------------------------------- 1 | 2 | System.Net 3 | System.Net.Http 4 | System.Security.Cryptography 5 | System.Security.Cryptography.X509Certificates 6 | System.Text.Json 7 | System.Threading.Tasks 8 | 9 | 10 | // Import this file into LinqPad and run to generate a full, self signed X509Certificate2 with a private key. 11 | // The password is within the script and will log out when ran. 12 | void Main() 13 | { 14 | var name = Guid.NewGuid().ToString("n"); 15 | var pwd = "emulator"; 16 | 17 | var cert = BuildX509Certificate(name); 18 | 19 | byte[] exported = cert.Export(X509ContentType.Pfx, pwd); 20 | 21 | File.WriteAllBytes(@"C:/Projects/cert.pfx", exported); 22 | 23 | var str = Convert.ToBase64String(exported); 24 | Console.WriteLine($"Exported X509Certificate2 {name} with password \"{pwd}\" to base64:"); 25 | Console.WriteLine(str); 26 | } 27 | 28 | public static X509Certificate2 BuildX509Certificate(string name) 29 | { 30 | int keySize = 2048; 31 | 32 | using var rsa = RSA.Create(keySize); 33 | rsa.ImportFromPem(RsaPem.FullPem); 34 | 35 | var certName = new X500DistinguishedName($"CN={name}"); 36 | 37 | var request = new CertificateRequest(certName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 38 | 39 | request.CertificateExtensions 40 | .Add(new X509KeyUsageExtension( 41 | X509KeyUsageFlags.DataEncipherment | 42 | X509KeyUsageFlags.KeyEncipherment | 43 | X509KeyUsageFlags.DigitalSignature, 44 | false) 45 | ); 46 | 47 | return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(365)); 48 | } 49 | 50 | public sealed class RsaPem 51 | { 52 | public const string FullPem = @" 53 | -----BEGIN PRIVATE KEY----- 54 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCofdCGPszXrWF7Jxwlhh6m9h56 55 | 9YhbscSK6I9zYkuBBgO0WbyZfZPimGNN5e+LqereEnAUa/ggDaTWldHjmStDz0O9dYr/9v8kWpxJ 56 | QmO6MpzBO/07yuUfCwRjdE8XzbG2BSEgLlVHyTgYDV83h9ThBN5xsiZ0bjqMKS1QrPu75uWBh+WH 57 | QmtDqWO7fx5Ilr/gdCGPBBRsA02/FBZbSRqKYfpFbz7aqtJhUD2QLeqzLq4fQ6JledqU6FDJ6XOP 58 | XEMlaoRaD3/s5odixOao0LlsTq7LWcNU1f2Au4t91lEGuzOLhlPH03sC18N+8Mg1vmOUl7589phG 59 | SFEyxO74sy2dAgMBAAECggEBAJ39O2ZlxJYIEXv09EOLO3q7FWGekbnJOs41uy0qYjoddaPK8TnL 60 | sruqwJLupGuFbKHHEClWBFep84LzANg1a4gt9QrWCPxyklN4U0uuYOzbQHlA0vcaDTXKktbe3Lsp 61 | ORXAQYt3ZqflWh/TihD74PUOJ7bcoYpTQbrjcYZQbcuF9JfX3Tv5spJKmLpHWKyL4XxBZizH92Zp 62 | OqgyuG/VwYyZRfU+9wPNs0pcHkgyFSF4b5n8b3iU6wNGmJJlfu9lOD8ev2L8aZUqdrqJOcCwK4rG 63 | BpS4tRXv4Hk9XTMGjl56xEyHk6juF2iMckLhYmRt6Q7/OfURxa2bLZdiByDOPlECgYEA2RCActeE 64 | CFQSCpNPapq6MsnrDETMP2CABMkwOpiWO2vOZs/Y5JWlIas2AlPx+CGL9WyxSAWtPRJN5FQXRgLS 65 | kcnUw6ZBMaGlcfa5a8Pd9ovAoLuPeCwpxFej+5HneDjWkRQsmRHKjXpWvruOHkhQdbv3T+smTsfd 66 | fgAIzERy69sCgYEAxrbdEYXW9FMAELNoqWo48PfLz1x0ouDRjHD+XgZCfNy1/fHXsFMZjo0UYT5D 67 | /Y1L7xfvYPf8DsVETsuUN8KLGBHR+wRf0zYf9Q663Hvk5XVJNnWBs0LHwgMxGNffC06HqGsA4RW1 68 | eeDN00JP5T3cyNmCN5iDcM4udPBzDlilgecCgYEAtVwxRkLFYUQE8usT7qkqq6bDibOtx8I0FEuY 69 | zUySMUGo6YP93zcdCp2HebhzsnMtAjj3goqjrSQvCngsHeXb082DxJiTXgmGN0sCr4SuXwFzR5iO 70 | jcSwfQkQzO+iK3Op6vulK5uO1liCQ8hnPOwEten//7kkf6xEZrNWpn0GXAMCgYEAqgyEo+En8M8y 71 | aBhPwWKwNa2oENxqx5OiXw+27ZlnvlhVuWoDDNYgMbgTL6BMKKeIyqNt60prvewcJ13ZidoGk+N0 72 | EN5Obn2L3Xbse4/ecmnq7BqkklXcge+fTUY2jgN23a4sA3JDaXfySw4dNuy4inxwDcmK+bbHVLUL 73 | kMRVZhMCgYA2GX8rOCkmlnJWmG3sYD0fP2KzSF5NxAFQq3F39IvwvJ+Inkx2agdZFBynjsg4VJSS 74 | tHfC5zD1uQ8sd/4hCbsK2yQuEsiS+j7Ij15MZvFdJCwxPKV99BTFuxIhnit5xEnqVzb7KNugwDp4 75 | Yu1oryGTUP3I2wF4ta4teRH/4y01Iw== 76 | -----END PRIVATE KEY-----"; 77 | } 78 | -------------------------------------------------------------------------------- /src/AzureKeyVaultEmulator.Shared/Models/Certificates/IssuerBundle.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using AzureKeyVaultEmulator.Shared.Constants; 6 | using AzureKeyVaultEmulator.Shared.Persistence.Interfaces; 7 | using AzureKeyVaultEmulator.Shared.Utilities; 8 | 9 | namespace AzureKeyVaultEmulator.Shared.Models.Certificates; 10 | 11 | // https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificate-issuer/get-certificate-issuer?view=rest-keyvault-certificates-7.4&tabs=HTTP#examples 12 | public sealed class IssuerBundle : INamedItem, IAttributedModel 13 | { 14 | [Key] 15 | [JsonIgnore] 16 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 17 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 18 | 19 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 20 | public string PersistedName { get; set; } = Guid.NewGuid().Neat(); 21 | 22 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 23 | public string PersistedVersion { get; set; } = Guid.NewGuid().Neat(); 24 | 25 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 26 | public bool Deleted { get; set; } = false; 27 | 28 | [JsonPropertyName("id")] 29 | public string Identifier => $"{AuthConstants.EmulatorUri}/certificates/issuers/{IssuerName}"; 30 | 31 | [JsonPropertyName("provider")] 32 | public string Provider { get; set; } = string.Empty; 33 | 34 | [JsonPropertyName("name")] 35 | public string IssuerName { get; set; } = string.Empty; 36 | 37 | [JsonPropertyName("credentials")] 38 | public IssuerCredentials Credentials { get; set; } = new(); 39 | 40 | [JsonPropertyName("attributes")] 41 | public IssuerAttributes Attributes { get; set; } = new(); 42 | 43 | [JsonPropertyName("org_details")] 44 | public OrganisationDetails? OrganisationDetails { get; set; } = new(); 45 | 46 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 47 | public IList Policies { get; set; } = []; 48 | } 49 | 50 | public sealed class IssuerAttributes : AttributeBase; 51 | 52 | public sealed class IssuerCredentials : IPersistedItem 53 | { 54 | [Key] 55 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 56 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 57 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 58 | 59 | [JsonPropertyName("account_id")] 60 | public string AccountId { get; set; } = string.Empty; 61 | 62 | [JsonPropertyName("pwd")] 63 | public string Password { get; set; } = string.Empty; 64 | } 65 | 66 | public sealed class OrganisationDetails : IPersistedItem 67 | { 68 | [Key] 69 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 70 | [JsonIgnore(Condition = JsonIgnoreCondition.Always)] 71 | public Guid PersistedId { get; set; } = Guid.NewGuid(); 72 | 73 | [JsonPropertyName("id")] 74 | public string Identifier { get; set; } = Guid.NewGuid().Neat(); 75 | 76 | public string BackingAdminDetails { get; set; } = "[]"; 77 | 78 | [JsonPropertyName("admin_details")] 79 | [NotMapped] 80 | public IEnumerable AdministratorDetails 81 | { 82 | get => JsonSerializer.Deserialize>(BackingAdminDetails) ?? []; 83 | set => BackingAdminDetails = JsonSerializer.Serialize(value); 84 | } 85 | } 86 | 87 | public sealed class AdministratorDetails 88 | { 89 | 90 | [JsonPropertyName("email")] 91 | public string Email { get; set; } = string.Empty; 92 | 93 | [JsonPropertyName("first_name")] 94 | public string FirstName { get; set; } = string.Empty; 95 | 96 | [JsonPropertyName("last_name")] 97 | public string LastName { get; set; } = string.Empty; 98 | 99 | [JsonPropertyName("phone")] 100 | public string Phone { get; set; } = string.Empty; 101 | } 102 | --------------------------------------------------------------------------------