feat: port session 07 — Protocol Parser, Auth extras (TPM/certidp/certstore), Internal utilities & data structures
Session 07 scope (5 features, 17 tests, ~1165 Go LOC): - Protocol/ParserTypes.cs: ParserState enum (79 states), PublishArgument, ParseContext - Protocol/IProtocolHandler.cs: handler interface decoupling parser from client - Protocol/ProtocolParser.cs: Parse(), ProtoSnippet(), OverMaxControlLineLimit(), ProcessPub/HeaderPub/RoutedMsgArgs/RoutedHeaderMsgArgs, ClonePubArg(), GetHeader() - tests/Protocol/ProtocolParserTests.cs: 17 tests via TestProtocolHandler stub Auth extras from session 06 (committed separately): - Auth/TpmKeyProvider.cs, Auth/CertificateIdentityProvider/, Auth/CertificateStore/ Internal utilities & data structures (session 06 overflow): - Internal/AccessTimeService.cs, ElasticPointer.cs, SystemMemory.cs, ProcessStatsProvider.cs - Internal/DataStructures/GenericSublist.cs, HashWheel.cs - Internal/DataStructures/SubjectTree.cs, SubjectTreeNode.cs, SubjectTreeParts.cs All 461 tests pass (460 unit + 1 integration). DB updated for features 2588-2592 and tests 2598-2614.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Error and debug message constants for the OCSP peer identity provider.
|
||||
/// Mirrors certidp/messages.go.
|
||||
/// </summary>
|
||||
public static class OcspMessages
|
||||
{
|
||||
// Returned errors
|
||||
public const string ErrIllegalPeerOptsConfig = "expected map to define OCSP peer options, got [{0}]";
|
||||
public const string ErrIllegalCacheOptsConfig = "expected map to define OCSP peer cache options, got [{0}]";
|
||||
public const string ErrParsingPeerOptFieldGeneric = "error parsing tls peer config, unknown field [\"{0}\"]";
|
||||
public const string ErrParsingPeerOptFieldTypeConversion = "error parsing tls peer config, conversion error: {0}";
|
||||
public const string ErrParsingCacheOptFieldTypeConversion = "error parsing OCSP peer cache config, conversion error: {0}";
|
||||
public const string ErrUnableToPlugTLSEmptyConfig = "unable to plug TLS verify connection, config is nil";
|
||||
public const string ErrMTLSRequired = "OCSP peer verification for client connections requires TLS verify (mTLS) to be enabled";
|
||||
public const string ErrUnableToPlugTLSClient = "unable to register client OCSP verification";
|
||||
public const string ErrUnableToPlugTLSServer = "unable to register server OCSP verification";
|
||||
public const string ErrCannotWriteCompressed = "error writing to compression writer: {0}";
|
||||
public const string ErrCannotReadCompressed = "error reading compression reader: {0}";
|
||||
public const string ErrTruncatedWrite = "short write on body ({0} != {1})";
|
||||
public const string ErrCannotCloseWriter = "error closing compression writer: {0}";
|
||||
public const string ErrParsingCacheOptFieldGeneric = "error parsing OCSP peer cache config, unknown field [\"{0}\"]";
|
||||
public const string ErrUnknownCacheType = "error parsing OCSP peer cache config, unknown type [{0}]";
|
||||
public const string ErrInvalidChainlink = "invalid chain link";
|
||||
public const string ErrBadResponderHTTPStatus = "bad OCSP responder http status: [{0}]";
|
||||
public const string ErrNoAvailOCSPServers = "no available OCSP servers";
|
||||
public const string ErrFailedWithAllRequests = "exhausted OCSP responders: {0}";
|
||||
|
||||
// Direct logged errors
|
||||
public const string ErrLoadCacheFail = "Unable to load OCSP peer cache: {0}";
|
||||
public const string ErrSaveCacheFail = "Unable to save OCSP peer cache: {0}";
|
||||
public const string ErrBadCacheTypeConfig = "Unimplemented OCSP peer cache type [{0}]";
|
||||
public const string ErrResponseCompressFail = "Unable to compress OCSP response for key [{0}]: {1}";
|
||||
public const string ErrResponseDecompressFail = "Unable to decompress OCSP response for key [{0}]: {1}";
|
||||
public const string ErrPeerEmptyNoEvent = "Peer certificate is nil, cannot send OCSP peer reject event";
|
||||
public const string ErrPeerEmptyAutoReject = "Peer certificate is nil, rejecting OCSP peer";
|
||||
|
||||
// Debug messages
|
||||
public const string DbgPlugTLSForKind = "Plugging TLS OCSP peer for [{0}]";
|
||||
public const string DbgNumServerChains = "Peer OCSP enabled: {0} TLS server chain(s) will be evaluated";
|
||||
public const string DbgNumClientChains = "Peer OCSP enabled: {0} TLS client chain(s) will be evaluated";
|
||||
public const string DbgLinksInChain = "Chain [{0}]: {1} total link(s)";
|
||||
public const string DbgSelfSignedValid = "Chain [{0}] is self-signed, thus peer is valid";
|
||||
public const string DbgValidNonOCSPChain = "Chain [{0}] has no OCSP eligible links, thus peer is valid";
|
||||
public const string DbgChainIsOCSPEligible = "Chain [{0}] has {1} OCSP eligible link(s)";
|
||||
public const string DbgChainIsOCSPValid = "Chain [{0}] is OCSP valid for all eligible links, thus peer is valid";
|
||||
public const string DbgNoOCSPValidChains = "No OCSP valid chains, thus peer is invalid";
|
||||
public const string DbgCheckingCacheForCert = "Checking OCSP peer cache for [{0}], key [{1}]";
|
||||
public const string DbgCurrentResponseCached = "Cached OCSP response is current, status [{0}]";
|
||||
public const string DbgExpiredResponseCached = "Cached OCSP response is expired, status [{0}]";
|
||||
public const string DbgOCSPValidPeerLink = "OCSP verify pass for [{0}]";
|
||||
public const string DbgMakingCARequest = "Making OCSP CA request to [{0}]";
|
||||
public const string DbgResponseExpired = "OCSP response expired: NextUpdate={0}, now={1}, skew={2}";
|
||||
public const string DbgResponseTTLExpired = "OCSP response TTL expired: expiry={0}, now={1}, skew={2}";
|
||||
public const string DbgResponseFutureDated = "OCSP response is future-dated: ThisUpdate={0}, now={1}, skew={2}";
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||
|
||||
/// <summary>OCSP certificate status values.</summary>
|
||||
/// <remarks>Mirrors the Go <c>ocsp.Good/Revoked/Unknown</c> constants (0/1/2).</remarks>
|
||||
[JsonConverter(typeof(OcspStatusAssertionJsonConverter))]
|
||||
public enum OcspStatusAssertion
|
||||
{
|
||||
Good = 0,
|
||||
Revoked = 1,
|
||||
Unknown = 2,
|
||||
}
|
||||
|
||||
/// <summary>JSON converter: serializes <see cref="OcspStatusAssertion"/> as lowercase string.</summary>
|
||||
public sealed class OcspStatusAssertionJsonConverter : JsonConverter<OcspStatusAssertion>
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, OcspStatusAssertion> StrToVal =
|
||||
new Dictionary<string, OcspStatusAssertion>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["good"] = OcspStatusAssertion.Good,
|
||||
["revoked"] = OcspStatusAssertion.Revoked,
|
||||
["unknown"] = OcspStatusAssertion.Unknown,
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<OcspStatusAssertion, string> ValToStr =
|
||||
new Dictionary<OcspStatusAssertion, string>
|
||||
{
|
||||
[OcspStatusAssertion.Good] = "good",
|
||||
[OcspStatusAssertion.Revoked] = "revoked",
|
||||
[OcspStatusAssertion.Unknown] = "unknown",
|
||||
};
|
||||
|
||||
public override OcspStatusAssertion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var s = reader.GetString() ?? string.Empty;
|
||||
return StrToVal.TryGetValue(s, out var v) ? v : OcspStatusAssertion.Unknown;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, OcspStatusAssertion value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(ValToStr.TryGetValue(value, out var s) ? s : "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of an OCSP status integer.
|
||||
/// Falls back to "unknown" for unrecognized values (never defaults to "good").
|
||||
/// </summary>
|
||||
public static class OcspStatusAssertionExtensions
|
||||
{
|
||||
public static string GetStatusAssertionStr(int statusInt) => statusInt switch
|
||||
{
|
||||
0 => "good",
|
||||
1 => "revoked",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Parsed OCSP peer configuration.</summary>
|
||||
public sealed class OcspPeerConfig
|
||||
{
|
||||
public static readonly TimeSpan DefaultAllowedClockSkew = TimeSpan.FromSeconds(30);
|
||||
public static readonly TimeSpan DefaultOCSPResponderTimeout = TimeSpan.FromSeconds(2);
|
||||
public static readonly TimeSpan DefaultTTLUnsetNextUpdate = TimeSpan.FromHours(1);
|
||||
|
||||
public bool Verify { get; set; } = false;
|
||||
public double Timeout { get; set; } = DefaultOCSPResponderTimeout.TotalSeconds;
|
||||
public double ClockSkew { get; set; } = DefaultAllowedClockSkew.TotalSeconds;
|
||||
public bool WarnOnly { get; set; } = false;
|
||||
public bool UnknownIsGood { get; set; } = false;
|
||||
public bool AllowWhenCAUnreachable { get; set; } = false;
|
||||
public double TTLUnsetNextUpdate { get; set; } = DefaultTTLUnsetNextUpdate.TotalSeconds;
|
||||
|
||||
/// <summary>Returns a new <see cref="OcspPeerConfig"/> with defaults populated.</summary>
|
||||
public static OcspPeerConfig Create() => new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a certificate chain link: a leaf certificate and its issuer,
|
||||
/// plus the OCSP web endpoints parsed from the leaf's AIA extension.
|
||||
/// </summary>
|
||||
public sealed class ChainLink
|
||||
{
|
||||
public X509Certificate2? Leaf { get; set; }
|
||||
public X509Certificate2? Issuer { get; set; }
|
||||
public IReadOnlyList<Uri>? OcspWebEndpoints { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsed OCSP response data. Mirrors the fields of <c>golang.org/x/crypto/ocsp.Response</c>
|
||||
/// needed by <see cref="OcspUtilities"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Full OCSP response parsing (DER/ASN.1) requires an additional library (e.g. Bouncy Castle).
|
||||
/// This type represents the already-parsed response for use in validation and caching logic.
|
||||
/// </remarks>
|
||||
public sealed class OcspResponse
|
||||
{
|
||||
public OcspStatusAssertion Status { get; init; }
|
||||
public DateTime ThisUpdate { get; init; }
|
||||
/// <summary><see cref="DateTime.MinValue"/> means "not set" (CA did not supply NextUpdate).</summary>
|
||||
public DateTime NextUpdate { get; init; }
|
||||
/// <summary>Optional delegated signer certificate (RFC 6960 §4.2.2.2).</summary>
|
||||
public X509Certificate2? Certificate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Neutral logging interface for plugin use. Mirrors the Go <c>certidp.Log</c> struct.</summary>
|
||||
public sealed class OcspLog
|
||||
{
|
||||
public Action<string, object[]>? Debugf { get; set; }
|
||||
public Action<string, object[]>? Noticef { get; set; }
|
||||
public Action<string, object[]>? Warnf { get; set; }
|
||||
public Action<string, object[]>? Errorf { get; set; }
|
||||
public Action<string, object[]>? Tracef { get; set; }
|
||||
|
||||
internal void Debug(string format, params object[] args) => Debugf?.Invoke(format, args);
|
||||
}
|
||||
|
||||
/// <summary>JSON-serializable certificate information.</summary>
|
||||
public sealed class CertInfo
|
||||
{
|
||||
[JsonPropertyName("subject")] public string? Subject { get; init; }
|
||||
[JsonPropertyName("issuer")] public string? Issuer { get; init; }
|
||||
[JsonPropertyName("fingerprint")] public string? Fingerprint { get; init; }
|
||||
[JsonPropertyName("raw")] public byte[]? Raw { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Net.Http;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||
|
||||
/// <summary>
|
||||
/// OCSP responder communication: fetches raw OCSP response bytes from CA endpoints.
|
||||
/// Mirrors certidp/ocsp_responder.go.
|
||||
/// </summary>
|
||||
public static class OcspResponder
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches an OCSP response from the responder URLs in <paramref name="link"/>.
|
||||
/// Tries each endpoint in order and returns the first successful response.
|
||||
/// </summary>
|
||||
/// <param name="link">Chain link containing leaf cert, issuer cert, and OCSP endpoints.</param>
|
||||
/// <param name="opts">Configuration (timeout, etc.).</param>
|
||||
/// <param name="log">Optional logger.</param>
|
||||
/// <param name="ocspRequest">DER-encoded OCSP request bytes to send.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Raw DER bytes of the OCSP response.</returns>
|
||||
public static async Task<byte[]> FetchOCSPResponseAsync(
|
||||
ChainLink link,
|
||||
OcspPeerConfig opts,
|
||||
byte[] ocspRequest,
|
||||
OcspLog? log = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (link.Leaf is null || link.Issuer is null)
|
||||
throw new ArgumentException(OcspMessages.ErrInvalidChainlink, nameof(link));
|
||||
if (link.OcspWebEndpoints is null || link.OcspWebEndpoints.Count == 0)
|
||||
throw new InvalidOperationException(OcspMessages.ErrNoAvailOCSPServers);
|
||||
|
||||
var timeout = TimeSpan.FromSeconds(opts.Timeout <= 0
|
||||
? OcspPeerConfig.DefaultOCSPResponderTimeout.TotalSeconds
|
||||
: opts.Timeout);
|
||||
|
||||
var reqEnc = EncodeOCSPRequest(ocspRequest);
|
||||
|
||||
using var hc = new HttpClient { Timeout = timeout };
|
||||
|
||||
Exception? lastError = null;
|
||||
foreach (var endpoint in link.OcspWebEndpoints)
|
||||
{
|
||||
var responderUrl = endpoint.ToString().TrimEnd('/');
|
||||
log?.Debug(OcspMessages.DbgMakingCARequest, responderUrl);
|
||||
|
||||
try
|
||||
{
|
||||
var url = $"{responderUrl}/{reqEnc}";
|
||||
using var response = await hc.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException(
|
||||
string.Format(OcspMessages.ErrBadResponderHTTPStatus, (int)response.StatusCode));
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
string.Format(OcspMessages.ErrFailedWithAllRequests, lastError?.Message), lastError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encodes the OCSP request DER bytes and URL-escapes the result
|
||||
/// for use as a path segment (RFC 6960 Appendix A.1).
|
||||
/// </summary>
|
||||
public static string EncodeOCSPRequest(byte[] reqDer) =>
|
||||
Uri.EscapeDataString(Convert.ToBase64String(reqDer));
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Utility methods for OCSP peer certificate validation.
|
||||
/// Mirrors certidp/certidp.go.
|
||||
/// </summary>
|
||||
public static class OcspUtilities
|
||||
{
|
||||
// OCSP AIA extension OID.
|
||||
private const string OidAuthorityInfoAccess = "1.3.6.1.5.5.7.1.1";
|
||||
// OCSPSigning extended key usage OID.
|
||||
private const string OidOcspSigning = "1.3.6.1.5.5.7.3.9";
|
||||
|
||||
/// <summary>Returns the SHA-256 fingerprint of the certificate's raw DER bytes, base64-encoded.</summary>
|
||||
public static string GenerateFingerprint(X509Certificate2 cert)
|
||||
{
|
||||
var hash = SHA256.HashData(cert.RawData);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters a list of URI strings to those that are valid HTTP or HTTPS URLs.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<Uri> GetWebEndpoints(IEnumerable<string> uris)
|
||||
{
|
||||
var result = new List<Uri>();
|
||||
foreach (var uri in uris)
|
||||
{
|
||||
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsed))
|
||||
continue;
|
||||
if (parsed.Scheme != "http" && parsed.Scheme != "https")
|
||||
continue;
|
||||
result.Add(parsed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the certificate subject in RDN sequence form, for logging.
|
||||
/// Not suitable for reliable cache matching.
|
||||
/// </summary>
|
||||
public static string GetSubjectDNForm(X509Certificate2? cert) =>
|
||||
cert is null ? string.Empty : cert.Subject;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the certificate issuer in RDN sequence form, for logging.
|
||||
/// Not suitable for reliable cache matching.
|
||||
/// </summary>
|
||||
public static string GetIssuerDNForm(X509Certificate2? cert) =>
|
||||
cert is null ? string.Empty : cert.Issuer;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the leaf certificate in the chain has OCSP responder endpoints
|
||||
/// in its Authority Information Access extension.
|
||||
/// Also populates <see cref="ChainLink.OcspWebEndpoints"/> on the link.
|
||||
/// </summary>
|
||||
public static bool CertOCSPEligible(ChainLink? link)
|
||||
{
|
||||
if (link?.Leaf is null || link.Leaf.RawData is not { Length: > 0 })
|
||||
return false;
|
||||
|
||||
var ocspUris = GetOcspUris(link.Leaf);
|
||||
var endpoints = GetWebEndpoints(ocspUris);
|
||||
if (endpoints.Count == 0)
|
||||
return false;
|
||||
|
||||
link.OcspWebEndpoints = endpoints;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the issuer certificate at position <paramref name="leafPos"/> + 1 in the chain.
|
||||
/// Returns null if the chain is too short or the leaf is self-signed.
|
||||
/// </summary>
|
||||
public static X509Certificate2? GetLeafIssuerCert(IReadOnlyList<X509Certificate2> chain, int leafPos)
|
||||
{
|
||||
if (chain.Count == 0 || leafPos < 0)
|
||||
return null;
|
||||
if (leafPos >= chain.Count - 1)
|
||||
return null;
|
||||
return chain[leafPos + 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the OCSP response is still current within the configured clock skew.
|
||||
/// </summary>
|
||||
public static bool OCSPResponseCurrent(OcspResponse response, OcspPeerConfig opts, OcspLog? log = null)
|
||||
{
|
||||
var skew = TimeSpan.FromSeconds(opts.ClockSkew < 0 ? OcspPeerConfig.DefaultAllowedClockSkew.TotalSeconds : opts.ClockSkew);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Check NextUpdate (when set by CA).
|
||||
if (response.NextUpdate != DateTime.MinValue && response.NextUpdate < now - skew)
|
||||
{
|
||||
log?.Debug(OcspMessages.DbgResponseExpired,
|
||||
response.NextUpdate.ToString("o"), now.ToString("o"), skew);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If NextUpdate not set, apply TTL from ThisUpdate.
|
||||
if (response.NextUpdate == DateTime.MinValue)
|
||||
{
|
||||
var ttl = TimeSpan.FromSeconds(opts.TTLUnsetNextUpdate < 0
|
||||
? OcspPeerConfig.DefaultTTLUnsetNextUpdate.TotalSeconds
|
||||
: opts.TTLUnsetNextUpdate);
|
||||
var expiry = response.ThisUpdate + ttl;
|
||||
if (expiry < now - skew)
|
||||
{
|
||||
log?.Debug(OcspMessages.DbgResponseTTLExpired,
|
||||
expiry.ToString("o"), now.ToString("o"), skew);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check ThisUpdate is not future-dated.
|
||||
if (response.ThisUpdate > now + skew)
|
||||
{
|
||||
log?.Debug(OcspMessages.DbgResponseFutureDated,
|
||||
response.ThisUpdate.ToString("o"), now.ToString("o"), skew);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the OCSP response was signed by a valid CA issuer or authorised delegate
|
||||
/// per RFC 6960 §4.2.2.2.
|
||||
/// </summary>
|
||||
public static bool ValidDelegationCheck(X509Certificate2? issuer, OcspResponse? response)
|
||||
{
|
||||
if (issuer is null || response is null)
|
||||
return false;
|
||||
|
||||
// Not a delegated response — the CA signed directly.
|
||||
if (response.Certificate is null)
|
||||
return true;
|
||||
|
||||
// Delegate is the same as the issuer — effectively a direct signing.
|
||||
if (response.Certificate.Thumbprint == issuer.Thumbprint)
|
||||
return true;
|
||||
|
||||
// Check the delegate has id-kp-OCSPSigning in its extended key usage.
|
||||
foreach (var ext in response.Certificate.Extensions)
|
||||
{
|
||||
if (ext is not X509EnhancedKeyUsageExtension eku)
|
||||
continue;
|
||||
foreach (var oid in eku.EnhancedKeyUsages)
|
||||
{
|
||||
if (oid.Value == OidOcspSigning)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private static IEnumerable<string> GetOcspUris(X509Certificate2 cert)
|
||||
{
|
||||
foreach (var ext in cert.Extensions)
|
||||
{
|
||||
if (ext.Oid?.Value != OidAuthorityInfoAccess)
|
||||
continue;
|
||||
foreach (var uri in ParseAiaUris(ext.RawData, isOcsp: true))
|
||||
yield return uri;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ParseAiaUris(byte[] aiaExtDer, bool isOcsp)
|
||||
{
|
||||
// OID for id-ad-ocsp: 1.3.6.1.5.5.7.48.1 → 2B 06 01 05 05 07 30 01
|
||||
byte[] ocspOid = [0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01];
|
||||
// OID for id-ad-caIssuers: 1.3.6.1.5.5.7.48.2 → 2B 06 01 05 05 07 30 02
|
||||
byte[] caIssuersOid = [0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02];
|
||||
|
||||
var target = isOcsp ? ocspOid : caIssuersOid;
|
||||
var result = new List<string>();
|
||||
int i = 0;
|
||||
|
||||
while (i < aiaExtDer.Length - target.Length - 4)
|
||||
{
|
||||
// Look for OID tag (0x06) followed by length matching our OID.
|
||||
if (aiaExtDer[i] == 0x06 && i + 1 < aiaExtDer.Length && aiaExtDer[i + 1] == target.Length)
|
||||
{
|
||||
var match = true;
|
||||
for (int k = 0; k < target.Length; k++)
|
||||
{
|
||||
if (aiaExtDer[i + 2 + k] != target[k]) { match = false; break; }
|
||||
}
|
||||
if (match)
|
||||
{
|
||||
// Next element should be context [6] IA5String (GeneralName uniformResourceIdentifier).
|
||||
int pos = i + 2 + target.Length;
|
||||
if (pos < aiaExtDer.Length && aiaExtDer[pos] == 0x86)
|
||||
{
|
||||
pos++;
|
||||
if (pos < aiaExtDer.Length)
|
||||
{
|
||||
int len = aiaExtDer[pos++];
|
||||
if (pos + len <= aiaExtDer.Length)
|
||||
{
|
||||
result.Add(System.Text.Encoding.ASCII.GetString(aiaExtDer, pos, len));
|
||||
i = pos + len;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright 2022-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.CertificateStore;
|
||||
|
||||
/// <summary>
|
||||
/// Windows certificate store location.
|
||||
/// Mirrors the Go certstore <c>StoreType</c> enum (windowsCurrentUser=1, windowsLocalMachine=2).
|
||||
/// </summary>
|
||||
public enum StoreType
|
||||
{
|
||||
Empty = 0,
|
||||
WindowsCurrentUser = 1,
|
||||
WindowsLocalMachine = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Certificate lookup criterion.
|
||||
/// Mirrors the Go certstore <c>MatchByType</c> enum (matchByIssuer=1, matchBySubject=2, matchByThumbprint=3).
|
||||
/// </summary>
|
||||
public enum MatchByType
|
||||
{
|
||||
Empty = 0,
|
||||
Issuer = 1,
|
||||
Subject = 2,
|
||||
Thumbprint = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result returned by <see cref="CertificateStoreService.TLSConfig"/>.
|
||||
/// Mirrors the data that the Go <c>TLSConfig</c> populates into <c>*tls.Config</c>.
|
||||
/// </summary>
|
||||
public sealed class CertStoreTlsResult
|
||||
{
|
||||
public CertStoreTlsResult(X509Certificate2 leaf, X509Certificate2Collection? caCerts = null)
|
||||
{
|
||||
Leaf = leaf;
|
||||
CaCerts = caCerts;
|
||||
}
|
||||
|
||||
/// <summary>The leaf certificate (with private key) to use as the server/client identity.</summary>
|
||||
public X509Certificate2 Leaf { get; }
|
||||
|
||||
/// <summary>Optional pool of CA certificates used to validate client certificates (mTLS).</summary>
|
||||
public X509Certificate2Collection? CaCerts { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error constants for the Windows certificate store module.
|
||||
/// Mirrors certstore/errors.go.
|
||||
/// </summary>
|
||||
public static class CertStoreErrors
|
||||
{
|
||||
public static readonly InvalidOperationException ErrBadCryptoStoreProvider =
|
||||
new("unable to open certificate store or store not available");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadRSAHashAlgorithm =
|
||||
new("unsupported RSA hash algorithm");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadSigningAlgorithm =
|
||||
new("unsupported signing algorithm");
|
||||
|
||||
public static readonly InvalidOperationException ErrStoreRSASigningError =
|
||||
new("unable to obtain RSA signature from store");
|
||||
|
||||
public static readonly InvalidOperationException ErrStoreECDSASigningError =
|
||||
new("unable to obtain ECDSA signature from store");
|
||||
|
||||
public static readonly InvalidOperationException ErrNoPrivateKeyStoreRef =
|
||||
new("unable to obtain private key handle from store");
|
||||
|
||||
public static readonly InvalidOperationException ErrExtractingPrivateKeyMetadata =
|
||||
new("unable to extract private key metadata");
|
||||
|
||||
public static readonly InvalidOperationException ErrExtractingECCPublicKey =
|
||||
new("unable to extract ECC public key from store");
|
||||
|
||||
public static readonly InvalidOperationException ErrExtractingRSAPublicKey =
|
||||
new("unable to extract RSA public key from store");
|
||||
|
||||
public static readonly InvalidOperationException ErrExtractingPublicKey =
|
||||
new("unable to extract public key from store");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadPublicKeyAlgorithm =
|
||||
new("unsupported public key algorithm");
|
||||
|
||||
public static readonly InvalidOperationException ErrExtractPropertyFromKey =
|
||||
new("unable to extract property from key");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadECCCurveName =
|
||||
new("unsupported ECC curve name");
|
||||
|
||||
public static readonly InvalidOperationException ErrFailedCertSearch =
|
||||
new("unable to find certificate in store");
|
||||
|
||||
public static readonly InvalidOperationException ErrFailedX509Extract =
|
||||
new("unable to extract x509 from certificate");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadMatchByType =
|
||||
new("cert match by type not implemented");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadCertStore =
|
||||
new("cert store type not implemented");
|
||||
|
||||
public static readonly InvalidOperationException ErrConflictCertFileAndStore =
|
||||
new("'cert_file' and 'cert_store' may not both be configured");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadCertStoreField =
|
||||
new("expected 'cert_store' to be a valid non-empty string");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadCertMatchByField =
|
||||
new("expected 'cert_match_by' to be a valid non-empty string");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadCertMatchField =
|
||||
new("expected 'cert_match' to be a valid non-empty string");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadCaCertMatchField =
|
||||
new("expected 'ca_certs_match' to be a valid non-empty string array");
|
||||
|
||||
public static readonly InvalidOperationException ErrBadCertMatchSkipInvalidField =
|
||||
new("expected 'cert_match_skip_invalid' to be a boolean");
|
||||
|
||||
public static readonly InvalidOperationException ErrOSNotCompatCertStore =
|
||||
new("cert_store not compatible with current operating system");
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright 2022-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from certstore/certstore.go and certstore/certstore_windows.go in
|
||||
// the NATS server Go source. The .NET implementation uses System.Security.
|
||||
// Cryptography.X509Certificates.X509Store in place of Win32 P/Invoke calls.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.CertificateStore;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the Windows certificate store for TLS certificate provisioning.
|
||||
/// Mirrors certstore/certstore.go and certstore/certstore_windows.go.
|
||||
///
|
||||
/// On non-Windows platforms all methods that require the Windows store throw
|
||||
/// <see cref="CertStoreErrors.ErrOSNotCompatCertStore"/>.
|
||||
/// </summary>
|
||||
public static class CertificateStoreService
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, StoreType> StoreMap =
|
||||
new Dictionary<string, StoreType>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["windowscurrentuser"] = StoreType.WindowsCurrentUser,
|
||||
["windowslocalmachine"] = StoreType.WindowsLocalMachine,
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, MatchByType> MatchByMap =
|
||||
new Dictionary<string, MatchByType>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["issuer"] = MatchByType.Issuer,
|
||||
["subject"] = MatchByType.Subject,
|
||||
["thumbprint"] = MatchByType.Thumbprint,
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Cross-platform parse helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Parses a cert_store string to a <see cref="StoreType"/>.
|
||||
/// Returns an error if the string is unrecognised or not valid on the current OS.
|
||||
/// Mirrors <c>ParseCertStore</c>.
|
||||
/// </summary>
|
||||
public static (StoreType store, Exception? error) ParseCertStore(string certStore)
|
||||
{
|
||||
if (!StoreMap.TryGetValue(certStore, out var st))
|
||||
return (StoreType.Empty, CertStoreErrors.ErrBadCertStore);
|
||||
|
||||
// All currently supported store types are Windows-only.
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return (StoreType.Empty, CertStoreErrors.ErrOSNotCompatCertStore);
|
||||
|
||||
return (st, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a cert_match_by string to a <see cref="MatchByType"/>.
|
||||
/// Mirrors <c>ParseCertMatchBy</c>.
|
||||
/// </summary>
|
||||
public static (MatchByType matchBy, Exception? error) ParseCertMatchBy(string certMatchBy)
|
||||
{
|
||||
if (!MatchByMap.TryGetValue(certMatchBy, out var mb))
|
||||
return (MatchByType.Empty, CertStoreErrors.ErrBadMatchByType);
|
||||
return (mb, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the issuer certificate for <paramref name="leaf"/> by building a chain.
|
||||
/// Returns null if the chain cannot be built or the leaf is self-signed.
|
||||
/// Mirrors <c>GetLeafIssuer</c>.
|
||||
/// </summary>
|
||||
public static X509Certificate2? GetLeafIssuer(X509Certificate2 leaf)
|
||||
{
|
||||
using var chain = new X509Chain();
|
||||
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||
|
||||
if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
|
||||
return null;
|
||||
|
||||
// chain.ChainElements[0] is the leaf; [1] is its issuer.
|
||||
return new X509Certificate2(chain.ChainElements[1].Certificate);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TLS configuration entry point
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Finds a certificate in the Windows certificate store matching the given criteria and
|
||||
/// returns a <see cref="CertStoreTlsResult"/> suitable for populating TLS options.
|
||||
///
|
||||
/// On non-Windows platforms throws <see cref="CertStoreErrors.ErrOSNotCompatCertStore"/>.
|
||||
/// Mirrors <c>TLSConfig</c> (certstore_windows.go).
|
||||
/// </summary>
|
||||
/// <param name="storeType">Which Windows store to use (CurrentUser or LocalMachine).</param>
|
||||
/// <param name="matchBy">How to match the certificate (Subject, Issuer, or Thumbprint).</param>
|
||||
/// <param name="certMatch">The match value (subject name, issuer name, or thumbprint hex).</param>
|
||||
/// <param name="caCertsMatch">Optional list of subject strings to locate CA certificates.</param>
|
||||
/// <param name="skipInvalid">If true, skip expired or not-yet-valid certificates.</param>
|
||||
public static CertStoreTlsResult TLSConfig(
|
||||
StoreType storeType,
|
||||
MatchByType matchBy,
|
||||
string certMatch,
|
||||
IReadOnlyList<string>? caCertsMatch = null,
|
||||
bool skipInvalid = false)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
throw CertStoreErrors.ErrOSNotCompatCertStore;
|
||||
if (storeType is not (StoreType.WindowsCurrentUser or StoreType.WindowsLocalMachine))
|
||||
throw CertStoreErrors.ErrBadCertStore;
|
||||
|
||||
var location = storeType == StoreType.WindowsCurrentUser
|
||||
? StoreLocation.CurrentUser
|
||||
: StoreLocation.LocalMachine;
|
||||
|
||||
// Find the leaf certificate.
|
||||
var leaf = matchBy switch
|
||||
{
|
||||
MatchByType.Subject or MatchByType.Empty => CertBySubject(certMatch, location, skipInvalid),
|
||||
MatchByType.Issuer => CertByIssuer(certMatch, location, skipInvalid),
|
||||
MatchByType.Thumbprint => CertByThumbprint(certMatch, location, skipInvalid),
|
||||
_ => throw CertStoreErrors.ErrBadMatchByType,
|
||||
} ?? throw CertStoreErrors.ErrFailedCertSearch;
|
||||
|
||||
// Optionally find CA certificates.
|
||||
X509Certificate2Collection? caPool = null;
|
||||
if (caCertsMatch is { Count: > 0 })
|
||||
caPool = CreateCACertsPool(location, caCertsMatch, skipInvalid);
|
||||
|
||||
return new CertStoreTlsResult(leaf, caPool);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Certificate search helpers (mirror winCertStore.certByXxx / certSearch)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first certificate in the personal (MY) store by subject name.
|
||||
/// Mirrors <c>certBySubject</c>.
|
||||
/// </summary>
|
||||
public static X509Certificate2? CertBySubject(string subject, StoreLocation location, bool skipInvalid) =>
|
||||
CertSearch(StoreName.My, location, X509FindType.FindBySubjectName, subject, skipInvalid);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first certificate in the personal (MY) store by issuer name.
|
||||
/// Mirrors <c>certByIssuer</c>.
|
||||
/// </summary>
|
||||
public static X509Certificate2? CertByIssuer(string issuer, StoreLocation location, bool skipInvalid) =>
|
||||
CertSearch(StoreName.My, location, X509FindType.FindByIssuerName, issuer, skipInvalid);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first certificate in the personal (MY) store by SHA-1 thumbprint (hex string).
|
||||
/// Mirrors <c>certByThumbprint</c>.
|
||||
/// </summary>
|
||||
public static X509Certificate2? CertByThumbprint(string thumbprint, StoreLocation location, bool skipInvalid) =>
|
||||
CertSearch(StoreName.My, location, X509FindType.FindByThumbprint, thumbprint, skipInvalid);
|
||||
|
||||
/// <summary>
|
||||
/// Searches Root, AuthRoot, and CA stores for certificates matching the given subject name.
|
||||
/// Returns all matching certificates across all three locations.
|
||||
/// Mirrors <c>caCertsBySubjectMatch</c>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<X509Certificate2> CaCertsBySubjectMatch(
|
||||
string subject,
|
||||
StoreLocation location,
|
||||
bool skipInvalid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(subject))
|
||||
throw CertStoreErrors.ErrBadCaCertMatchField;
|
||||
|
||||
var results = new List<X509Certificate2>();
|
||||
var searchLocations = new[] { StoreName.Root, StoreName.AuthRoot, StoreName.CertificateAuthority };
|
||||
|
||||
foreach (var storeName in searchLocations)
|
||||
{
|
||||
var cert = CertSearch(storeName, location, X509FindType.FindBySubjectName, subject, skipInvalid);
|
||||
if (cert != null)
|
||||
results.Add(cert);
|
||||
}
|
||||
|
||||
if (results.Count == 0)
|
||||
throw CertStoreErrors.ErrFailedCertSearch;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core certificate search — opens the specified store and finds a matching certificate.
|
||||
/// Returns null if not found.
|
||||
/// Mirrors <c>certSearch</c>.
|
||||
/// </summary>
|
||||
public static X509Certificate2? CertSearch(
|
||||
StoreName storeName,
|
||||
StoreLocation storeLocation,
|
||||
X509FindType findType,
|
||||
string findValue,
|
||||
bool skipInvalid)
|
||||
{
|
||||
using var store = new X509Store(storeName, storeLocation, OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
|
||||
var certs = store.Certificates.Find(findType, findValue, validOnly: skipInvalid);
|
||||
if (certs.Count == 0)
|
||||
return null;
|
||||
|
||||
// Pick first that has a private key (mirrors certKey requirement in Go).
|
||||
foreach (var cert in certs)
|
||||
{
|
||||
if (cert.HasPrivateKey)
|
||||
return cert;
|
||||
}
|
||||
|
||||
// Fall back to first even without private key (e.g. CA cert lookup).
|
||||
return certs[0];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CA cert pool builder (mirrors createCACertsPool)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Builds a collection of CA certificates from the trusted Root, AuthRoot, and CA stores
|
||||
/// for each subject name in <paramref name="caCertsMatch"/>.
|
||||
/// Mirrors <c>createCACertsPool</c>.
|
||||
/// </summary>
|
||||
public static X509Certificate2Collection CreateCACertsPool(
|
||||
StoreLocation location,
|
||||
IReadOnlyList<string> caCertsMatch,
|
||||
bool skipInvalid)
|
||||
{
|
||||
var pool = new X509Certificate2Collection();
|
||||
var failCount = 0;
|
||||
|
||||
foreach (var subject in caCertsMatch)
|
||||
{
|
||||
try
|
||||
{
|
||||
var matches = CaCertsBySubjectMatch(subject, location, skipInvalid);
|
||||
foreach (var cert in matches)
|
||||
pool.Add(cert);
|
||||
}
|
||||
catch
|
||||
{
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failCount == caCertsMatch.Count)
|
||||
throw new InvalidOperationException("unable to match any CA certificate");
|
||||
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
61
dotnet/src/ZB.MOM.NatsNet.Server/Auth/TpmKeyProvider.cs
Normal file
61
dotnet/src/ZB.MOM.NatsNet.Server/Auth/TpmKeyProvider.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Provides JetStream encryption key management via the Trusted Platform Module (TPM).
|
||||
/// Windows only — non-Windows platforms throw <see cref="PlatformNotSupportedException"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On Windows, the full implementation requires the Tpm2Lib NuGet package and accesses
|
||||
/// the TPM to seal/unseal keys using PCR-based authorization. The sealed public and
|
||||
/// private key blobs are persisted to disk as JSON.
|
||||
/// </remarks>
|
||||
public static class TpmKeyProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads (or creates) the JetStream encryption key from the TPM.
|
||||
/// On first call (key file does not exist), generates a new NKey seed, seals it to the
|
||||
/// TPM, and writes the blobs to <paramref name="jsKeyFile"/>.
|
||||
/// On subsequent calls, reads the blobs from disk and unseals them using the TPM.
|
||||
/// </summary>
|
||||
/// <param name="srkPassword">Storage Root Key password (may be empty).</param>
|
||||
/// <param name="jsKeyFile">Path to the persisted key blobs JSON file.</param>
|
||||
/// <param name="jsKeyPassword">Password used to seal/unseal the JetStream key.</param>
|
||||
/// <param name="pcr">PCR index to bind the authorization policy to.</param>
|
||||
/// <returns>The JetStream encryption key seed string.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown on non-Windows platforms.</exception>
|
||||
public static string LoadJetStreamEncryptionKeyFromTpm(
|
||||
string srkPassword,
|
||||
string jsKeyFile,
|
||||
string jsKeyPassword,
|
||||
int pcr)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
throw new PlatformNotSupportedException("TPM functionality is not supported on this platform.");
|
||||
|
||||
// Windows implementation requires Tpm2Lib NuGet package.
|
||||
// Add <PackageReference Include="Tpm2Lib" Version="*" /> to the .csproj
|
||||
// under a Windows-conditional ItemGroup before enabling this path.
|
||||
throw new PlatformNotSupportedException(
|
||||
"TPM functionality is not supported on this platform. " +
|
||||
"On Windows, add Tpm2Lib NuGet package and implement via tpm2.OpenTPM().");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persisted TPM key blobs stored on disk as JSON.
|
||||
/// </summary>
|
||||
internal sealed class NatsPersistedTpmKeys
|
||||
{
|
||||
[JsonPropertyName("version")]
|
||||
public int Version { get; set; }
|
||||
|
||||
[JsonPropertyName("private_key")]
|
||||
public byte[] PrivateKey { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("public_key")]
|
||||
public byte[] PublicKey { get; set; } = [];
|
||||
}
|
||||
Reference in New Issue
Block a user