96 lines
6.3 KiB
C#
96 lines
6.3 KiB
C#
using System.Diagnostics;
|
|
using System.Collections;
|
|
using System.Reflection;
|
|
using System.Runtime;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Configuration;
|
|
using Opc.Ua.Security.Certificates;
|
|
|
|
[System.Runtime.Versioning.SupportedOSPlatform("macos")]
|
|
internal static class ImportSeed
|
|
{
|
|
const string Uri = "urn:localhost:NativeRetention.Control";
|
|
static string Hash(Certificate cert) => Convert.ToHexString(SHA256.HashData(cert.RawData)).ToLowerInvariant();
|
|
static void Require(bool yes, string message) { if (!yes) throw new InvalidOperationException(message); }
|
|
static ApplicationConfiguration Configuration(string pki, ITelemetryContext telemetry) => new(telemetry)
|
|
{
|
|
ApplicationName = "NativeRetention.Control", ApplicationUri = Uri, ApplicationType = ApplicationType.Client,
|
|
SecurityConfiguration = new SecurityConfiguration
|
|
{
|
|
ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory,
|
|
StorePath = Path.Combine(pki, "own"), SubjectName = "CN=NativeRetention.Control" },
|
|
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "trusted") },
|
|
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "issuers") },
|
|
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "rejected") },
|
|
AutoAcceptUntrustedCertificates = false,
|
|
},
|
|
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15000 },
|
|
};
|
|
static async Task<ApplicationInstance> Open(string pki, ITelemetryContext telemetry, CancellationToken ct)
|
|
{
|
|
var app = new ApplicationInstance(telemetry) { ApplicationType = ApplicationType.Client,
|
|
ApplicationName = "NativeRetention.Control", ApplicationConfiguration = Configuration(pki, telemetry) };
|
|
try
|
|
{
|
|
await app.ApplicationConfiguration!.ValidateAsync(ApplicationType.Client, ct);
|
|
Require(await app.CheckApplicationInstanceCertificatesAsync(true, null, ct), "Normal application certificate check failed");
|
|
return app;
|
|
}
|
|
catch { await app.DisposeAsync(); throw; }
|
|
}
|
|
static bool ManagerDisposed(ICertificateManager manager)
|
|
{
|
|
Require(manager.GetType().FullName == "Opc.Ua.CertificateManager" && manager.GetType().Assembly.GetName().Version?.ToString() == "2.0.1.0", "Exact SDK diagnostic layout changed");
|
|
var field = manager.GetType().GetField("m_disposed", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
|
Require(field?.FieldType == typeof(bool), "Exact SDK disposal bool missing");
|
|
return (bool)field!.GetValue(manager)!;
|
|
}
|
|
static void PrivateFile(string path, byte[] bytes)
|
|
{
|
|
using var stream = new FileStream(path, new FileStreamOptions { Mode = FileMode.CreateNew,
|
|
Access = FileAccess.Write, UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite });
|
|
stream.Write(bytes);
|
|
}
|
|
public static async Task<int> Main(string[] args)
|
|
{
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
|
ApplicationInstance? app = null;
|
|
try
|
|
{
|
|
Require(OperatingSystem.IsMacOS() && Environment.Version.ToString() == "10.0.14", "Expected matched macOS/.NET10.0.14 seed");
|
|
var root = Path.GetFullPath(args.Single());
|
|
Require(Directory.Exists(root) && (File.GetUnixFileMode(root) & (UnixFileMode)63) == 0, "Expected private fresh seed directory");
|
|
app = await Open(Path.Combine(root, "pki"), DefaultTelemetry.Create(_ => { }), deadline.Token);
|
|
var manager = app.ApplicationConfiguration!.CertificateManager;
|
|
string leaf;
|
|
using (var entry = manager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256))
|
|
{
|
|
Require(entry is not null && entry.Certificate.HasPrivateKey, "Expected SDK-generated private certificate");
|
|
var certificate = entry!.Certificate; leaf = Hash(certificate);
|
|
using var rsa = certificate.GetRSAPrivateKey(); using var publicKey = certificate.GetRSAPublicKey();
|
|
Require(rsa is not null && publicKey is not null, "Expected normal SDK RSA certificate");
|
|
var challenge = "native-import-seed-diagnostic"u8.ToArray();
|
|
var signature = rsa!.SignData(challenge, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
Require(publicKey!.VerifyData(challenge, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), "Seed key mismatch");
|
|
var privateKey = rsa.ExportRSAPrivateKey();
|
|
try { PrivateFile(Path.Combine(root, "leaf.der"), certificate.RawData); PrivateFile(Path.Combine(root, "private.pkcs1"), privateKey); }
|
|
finally { CryptographicOperations.ZeroMemory(privateKey); }
|
|
}
|
|
await app.DisposeAsync().AsTask().WaitAsync(deadline.Token); app = null;
|
|
Require(ManagerDisposed(manager) && Certificate.InstancesLeaked == 0, "Seed SDK ownership remains");
|
|
Console.WriteLine(JsonSerializer.Serialize(new { kind = "seed", leafSha256 = leaf, runtime = Environment.Version.ToString(),
|
|
sdkVersion = typeof(Certificate).Assembly.GetName().Version!.ToString(), managerDisposed = true,
|
|
certificateCoresOutstanding = Certificate.InstancesLeaked, keyFormat = "PKCS1-private-RSA", privateFileMode = "0600",
|
|
cryptographyRuntime = new { informationalVersion = typeof(X509Certificate2).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion,
|
|
sha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(typeof(X509Certificate2).Assembly.Location))).ToLowerInvariant() } }));
|
|
return 0;
|
|
}
|
|
catch(Exception error) { Console.WriteLine(JsonSerializer.Serialize(new {kind="failed", errorType=error.GetType().FullName, message=error.Message}));return 1; }
|
|
finally { if(app is not null) await app.DisposeAsync(); }
|
|
}
|
|
}
|