Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,12 @@ logging:
| `CRYPT_API_KEY` | API key for server authentication |
| `CRYPT_API_KEY_HEADER` | Custom API key header name (default: X-API-Key) |
| `CRYPT_USE_MTLS` | Enable mutual TLS authentication (true/false) |
| `CRYPT_CERT_SUBJECT` | Client certificate subject name for mTLS |
| `CRYPT_CERT_THUMBPRINT` | Client certificate thumbprint for mTLS |
| `CRYPT_CERT_SUBJECT` | Client certificate subject name for mTLS (Cert Store) |
| `CRYPT_CERT_THUMBPRINT` | Client certificate thumbprint for mTLS (Cert Store) |
| `CRYPT_PFX_PATH` | Path to client certificate PFX file for mTLS (preferred file-based option) |
| `CRYPT_PFX_PASSWORD_CRED` | Name of Windows Credential Manager entry holding the PFX passphrase |
| `CRYPT_CLIENT_CERT_PATH` | Path to client certificate PEM file for mTLS (least preferred file-based option) |
| `CRYPT_CLIENT_KEY_PATH` | Path to client private key PEM file (paired with above) |

### Authentication

Expand All @@ -217,7 +221,11 @@ Or via environment variables: `CRYPT_API_KEY` and `CRYPT_API_KEY_HEADER`.

**Mutual TLS (mTLS):**

Similar to Mac Crypt's `CommonNameForEscrow` feature. Uses a client certificate from the Windows Certificate Store.
Set `use_mtls: true` (or `CRYPT_USE_MTLS=true`) and pick one of the three strategies below. They are tried in the order listed — most secure first — and the first one that succeeds wins. If you configure multiple, the higher-ranked strategy is used.

#### 1. Windows Certificate Store (preferred)

Similar to Mac Crypt's `CommonNameForEscrow` feature. The private key is protected by DPAPI, can be marked non-exportable at import time, and integrates with the Windows PKI lifecycle. Deployable via Intune / Group Policy.

```yaml
server:
Expand All @@ -228,7 +236,47 @@ server:
certificate_store_name: My
```

Or via environment variables: `CRYPT_USE_MTLS`, `CRYPT_CERT_SUBJECT`, `CRYPT_CERT_THUMBPRINT`.
Environment variables: `CRYPT_CERT_SUBJECT`, `CRYPT_CERT_THUMBPRINT`.

#### 2. PFX file + Credential Manager passphrase

Use when importing into the Cert Store isn't feasible but you still want encrypted-at-rest key material. The passphrase lives in Windows Credential Manager (DPAPI-protected) and is **never** stored in YAML, environment variables, or the registry.

Provision the passphrase once with `cmdkey`:

```cmd
cmdkey /generic:CryptPfxPassword /user:cryptescrow /pass:<pfx-passphrase>
```

Then configure:

```yaml
server:
auth:
use_mtls: true
pfx_path: C:\ProgramData\ManagedEncryption\client.pfx
pfx_password_credential: CryptPfxPassword # matches the /generic: value above
```

Environment variables: `CRYPT_PFX_PATH`, `CRYPT_PFX_PASSWORD_CRED`. If the PFX has no passphrase, omit `pfx_password_credential`.

#### 3. PEM + .key file (least preferred)

The private key sits in plaintext on disk, protected only by filesystem ACLs. Only use this when the other two options aren't available, and lock the key file down so only `SYSTEM` (or the service account) can read it:

```cmd
icacls C:\ProgramData\ManagedEncryption\client.key /inheritance:r /grant:r SYSTEM:R
```

```yaml
server:
auth:
use_mtls: true
client_cert_path: C:\ProgramData\ManagedEncryption\client.pem
client_key_path: C:\ProgramData\ManagedEncryption\client.key
```

Environment variables: `CRYPT_CLIENT_CERT_PATH`, `CRYPT_CLIENT_KEY_PATH`.

### Registry Configuration (CSP/OMA-URI)

Expand All @@ -255,6 +303,10 @@ Enterprise policies can be deployed via Intune CSP/OMA-URI to these registry loc
| `UseMtls` | String/DWORD | Enable mutual TLS authentication (true/1 or false/0) |
| `CertificateSubject` | String | Client certificate subject name for mTLS |
| `CertificateThumbprint` | String | Client certificate thumbprint for mTLS |
| `PfxPath` | String | Path to client PFX file (preferred file-based mTLS option) |
| `PfxPasswordCredential` | String | Windows Credential Manager target name holding the PFX passphrase |
| `ClientCertPath` | String | Path to client certificate PEM file (least preferred file-based mTLS option) |
| `ClientKeyPath` | String | Path to client private key PEM file (paired with `ClientCertPath`) |

**Intune Custom OMA-URI Example:**
- OMA-URI: `./Device/Vendor/MSFT/Registry/HKLM/SOFTWARE/Policies/Crypt/ManagedEncryption/ServerUrl`
Expand Down
1 change: 1 addition & 0 deletions src/CryptEscrow/CryptEscrow.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>
Expand Down
106 changes: 105 additions & 1 deletion src/CryptEscrow/Services/ConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,79 @@ public static bool GetUseMtls()
return config?.Server?.Auth?.CertificateThumbprint;
}

/// <summary>
/// Gets the path to a client certificate PEM file for mTLS.
/// Priority: Environment variable > Registry > YAML config
/// </summary>
public static string? GetClientCertPath()
{
var envValue = Environment.GetEnvironmentVariable("CRYPT_CLIENT_CERT_PATH");
if (!string.IsNullOrWhiteSpace(envValue))
return envValue;

var regValue = GetRegistryValue("ClientCertPath");
if (!string.IsNullOrWhiteSpace(regValue))
return regValue;

var config = LoadConfig();
return config?.Server?.Auth?.ClientCertPath;
}

/// <summary>
/// Gets the path to the client private key PEM file for mTLS (paired with ClientCertPath).
/// Priority: Environment variable > Registry > YAML config
/// </summary>
public static string? GetClientKeyPath()
{
var envValue = Environment.GetEnvironmentVariable("CRYPT_CLIENT_KEY_PATH");
if (!string.IsNullOrWhiteSpace(envValue))
return envValue;

var regValue = GetRegistryValue("ClientKeyPath");
if (!string.IsNullOrWhiteSpace(regValue))
return regValue;

var config = LoadConfig();
return config?.Server?.Auth?.ClientKeyPath;
}

/// <summary>
/// Gets the path to a client certificate PFX file for mTLS. Preferred over PEM because
/// the private key is encrypted at rest and the passphrase is pulled from Credential Manager.
/// Priority: Environment variable > Registry > YAML config
/// </summary>
public static string? GetPfxPath()
{
var envValue = Environment.GetEnvironmentVariable("CRYPT_PFX_PATH");
if (!string.IsNullOrWhiteSpace(envValue))
return envValue;

var regValue = GetRegistryValue("PfxPath");
if (!string.IsNullOrWhiteSpace(regValue))
return regValue;

var config = LoadConfig();
return config?.Server?.Auth?.PfxPath;
}

/// <summary>
/// Gets the name of the Windows Credential Manager entry holding the PFX passphrase.
/// Priority: Environment variable > Registry > YAML config
/// </summary>
public static string? GetPfxPasswordCredential()
{
var envValue = Environment.GetEnvironmentVariable("CRYPT_PFX_PASSWORD_CRED");
if (!string.IsNullOrWhiteSpace(envValue))
return envValue;

var regValue = GetRegistryValue("PfxPasswordCredential");
if (!string.IsNullOrWhiteSpace(regValue))
return regValue;

var config = LoadConfig();
return config?.Server?.Auth?.PfxPasswordCredential;
}

/// <summary>
/// Gets the full authentication configuration.
/// </summary>
Expand All @@ -360,7 +433,11 @@ public static AuthConfig GetAuthConfig()
CertificateSubject = GetCertificateSubject(),
CertificateThumbprint = GetCertificateThumbprint(),
CertificateStoreLocation = config?.Server?.Auth?.CertificateStoreLocation ?? "LocalMachine",
CertificateStoreName = config?.Server?.Auth?.CertificateStoreName ?? "My"
CertificateStoreName = config?.Server?.Auth?.CertificateStoreName ?? "My",
ClientCertPath = GetClientCertPath(),
ClientKeyPath = GetClientKeyPath(),
PfxPath = GetPfxPath(),
PfxPasswordCredential = GetPfxPasswordCredential()
};
}

Expand Down Expand Up @@ -575,6 +652,33 @@ public class AuthConfig
/// Default: My (Personal certificates).
/// </summary>
public string CertificateStoreName { get; set; } = "My";

/// <summary>
/// Path to a client certificate PEM file for mTLS. Least preferred file-based option
/// because the paired private key sits in plaintext on disk. Must be used together
/// with <see cref="ClientKeyPath"/>.
/// </summary>
public string? ClientCertPath { get; set; }

/// <summary>
/// Path to the client private key PEM file for mTLS. Paired with <see cref="ClientCertPath"/>.
/// Lock this file down with a restrictive ACL — the key is unencrypted at rest.
/// </summary>
public string? ClientKeyPath { get; set; }

/// <summary>
/// Path to a client certificate PFX (PKCS#12) file for mTLS. Preferred file-based option
/// because the private key is encrypted at rest. The decryption passphrase is read from
/// Windows Credential Manager via <see cref="PfxPasswordCredential"/>.
/// </summary>
public string? PfxPath { get; set; }

/// <summary>
/// Name of a generic Windows Credential Manager entry whose password blob holds the PFX
/// passphrase. The password is never stored in YAML, environment variables, or the registry.
/// Provision with <c>cmdkey /generic:&lt;name&gt; /user:&lt;anything&gt; /pass:&lt;secret&gt;</c>.
/// </summary>
public string? PfxPasswordCredential { get; set; }
}

public class EscrowConfig
Expand Down
85 changes: 85 additions & 0 deletions src/CryptEscrow/Services/CredentialManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Runtime.InteropServices;
using Serilog;

namespace CryptEscrow.Services;

/// <summary>
/// Minimal read-only wrapper over Windows Credential Manager (advapi32!CredReadW).
/// Used to retrieve the passphrase for a PFX client certificate without storing
/// it in YAML, environment variables, or the registry.
/// </summary>
internal static partial class CredentialManager
{
private const uint CRED_TYPE_GENERIC = 1;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CREDENTIAL
{
public uint Flags;
public uint Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public uint CredentialBlobSize;
public IntPtr CredentialBlob;
public uint Persist;
public uint AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}

[LibraryImport("advapi32.dll", EntryPoint = "CredReadW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CredRead(string target, uint type, uint reservedFlag, out IntPtr credentialPtr);

[LibraryImport("advapi32.dll", EntryPoint = "CredFree")]
private static partial void CredFree(IntPtr cred);

/// <summary>
/// Reads the password blob of a generic credential by target name.
/// Returns null if the credential doesn't exist or can't be read.
/// </summary>
/// <param name="targetName">
/// The target name of the Windows Credential Manager entry. Create with
/// <c>cmdkey /generic:&lt;name&gt; /user:&lt;anything&gt; /pass:&lt;secret&gt;</c>.
/// </param>
public static string? ReadGenericPassword(string targetName)
{
if (string.IsNullOrWhiteSpace(targetName))
return null;

if (!CredRead(targetName, CRED_TYPE_GENERIC, 0, out var ptr))
{
var err = Marshal.GetLastWin32Error();
Log.Warning(
"Credential Manager: target '{Target}' not found (Win32 error {Err})",
targetName, err);
return null;
}

try
{
var cred = Marshal.PtrToStructure<CREDENTIAL>(ptr);
if (cred.CredentialBlobSize == 0 || cred.CredentialBlob == IntPtr.Zero)
return string.Empty;

// CredentialBlob is raw bytes; cmdkey and PowerShell's Credential
// Manager APIs store passphrases as UTF-16LE.
var bytes = new byte[cred.CredentialBlobSize];
Marshal.Copy(cred.CredentialBlob, bytes, 0, bytes.Length);
try
{
return System.Text.Encoding.Unicode.GetString(bytes);
}
finally
{
Array.Clear(bytes);
}
}
finally
{
CredFree(ptr);
}
}
}
Loading