SamuelFisher / TerraformPluginDotNet

Write Terraform providers in C#.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Getting this to work in Windows

PaulVipond opened this issue · comments

Hey Samuel, thought I'd give you some feedback on getting the provider to work in 'production' mode on Windows when Terraform calls it directly. It would be nice if you could add this as Windows notes to the docs. Here's the things I needed to do:

  • Initially, I wasn't getting logs or anything, so I added this to Main(). When Terraform launches the .exe I get an 'attach debugger' prompt and I can just run it through Visual Studio
    Debugger.Launch();   // Comment this in to be able to debug the provider when Terraform calls it
  • When debugging the app startup, you have no access to the output, so it's best to use a file based logger as you do, but assert control over where the output is
      var exeDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);        
      string logFile = appSettings.GetValue<string>("Serilog:WriteTo:0:Args:path");
      Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(appSettings)
                .WriteTo.File(Path.Combine(exeDirectory, logFile))
                .CreateLogger();
  • When using appsettings.json make sure you get this from the apps folder, as you have no idea what the current folder might be:
            var exeDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
            var appSettings = new ConfigurationBuilder()
                .SetBasePath(exeDirectory)
                .AddJsonFile("appsettings.json", optional: false)
                .Build();

  • In-memory self-signed certificates don't work on Windows, so I've expanded on the certificate helper below. See this discussion: dotnet/runtime#23749

The startup looks like:

    var exeDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
    string certFileStem = Path.Combine(exeDirectory, "tfcert");
    Cert = CertificateGenerator.GenerateSelfSignedCertificate(certFileStem, "CN=127.0.0.1", "CN=root ca", 
    CertificateGenerator.GeneratePrivateKey());

And the helper class:


    public static class CertificateGenerator
    {
        private const int KeyStrength = 2048;

        public static X509Certificate2 GenerateSelfSignedCertificate(string fileStem, string subjectName, string issuerName, AsymmetricKeyParameter issuerPrivKey)
        {
            // This convoluted process of creating a certificate, saving it and reimporting it is required on
            // Windows because of a bug in Windows SSL handling, where in-memory certificates are not handled
            // correctly: https://github.com/dotnet/runtime/issues/23749
            // The presenting error is: "No credentials are available in the security package"
            //
            // "After discussions with the SCHANNEL team, it has been confirmed that this won't work in the
            // current versions of Windows due to SCHANNEL's cross-process architecture with LSASS.EXE.
            // The in-memory TLS client certificate private key is not marshaled between SCHANNEL and LSASS.
            // That is why SEC_E_NO_CREDENTIALS is returned from SCHANNEL AcquireCredentialHandle() call."

            var inMemorycert = CreateSelfSignedCertificate(subjectName, issuerName, issuerPrivKey);
            SaveCertificateToPemAndKeyFiles(inMemorycert, fileStem);
            var importedCert = CreateFromPublicPrivateKey($"{fileStem}.pem", $"{fileStem}.key");
            File.Delete($"{fileStem}.pem");
            File.Delete($"{fileStem}.key");

            return new X509Certificate2(importedCert.Export(X509ContentType.Pkcs12));
        }

        public static AsymmetricKeyParameter GeneratePrivateKey()
        {
            var randomGenerator = new CryptoApiRandomGenerator();
            var random = new SecureRandom(randomGenerator);

            // Generate Key
            var keyGenerationParameters = new KeyGenerationParameters(random, KeyStrength);
            var keyPairGenerator = new RsaKeyPairGenerator();
            keyPairGenerator.Init(keyGenerationParameters);
            return keyPairGenerator.GenerateKeyPair().Private;
        }

        public static X509Certificate2 CreateSelfSignedCertificate(string subjectName, string issuerName, AsymmetricKeyParameter issuerPrivKey)
        {
            var randomGenerator = new CryptoApiRandomGenerator();
            var random = new SecureRandom(randomGenerator);
            var certificateGenerator = new X509V3CertificateGenerator();

            // Serial Number
            var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
            certificateGenerator.SetSerialNumber(serialNumber);

            // Issuer and SN
            var subjectDN = new X509Name(subjectName);
            var issuerDN = new X509Name(issuerName);
            certificateGenerator.SetIssuerDN(issuerDN);
            certificateGenerator.SetSubjectDN(subjectDN);

            // SAN
            var subjectAltName = new GeneralNames(new GeneralName(GeneralName.DnsName, "localhost"));
            certificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName);

            // Validity
            var notBefore = DateTime.UtcNow.Date;
            var notAfter = notBefore.AddYears(2);
            certificateGenerator.SetNotBefore(notBefore);
            certificateGenerator.SetNotAfter(notAfter);

            // Public Key
            var keyGenerationParameters = new KeyGenerationParameters(random, KeyStrength);
            var keyPairGenerator = new RsaKeyPairGenerator();
            keyPairGenerator.Init(keyGenerationParameters);
            var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
            certificateGenerator.SetPublicKey(subjectKeyPair.Public);

            // Sign certificate
            var signatureFactory = new Asn1SignatureFactory("SHA256WithRSA", issuerPrivKey, random);
            var certificate = certificateGenerator.Generate(signatureFactory);
            var x509 = new X509Certificate2(certificate.GetEncoded(), (string)null, X509KeyStorageFlags.Exportable);

            // Private key
            var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);

            var seq = (Asn1Sequence)Asn1Object.FromByteArray(privateKeyInfo.ParsePrivateKey().GetDerEncoded());
            if (seq.Count != 9)
            {
                throw new PemException("Invalid RSA private key");
            }

            var rsa = RsaPrivateKeyStructure.GetInstance(seq);
            var rsaparams = new RsaPrivateCrtKeyParameters(rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent, rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient);

            var parms = DotNetUtilities.ToRSAParameters(rsaparams);
            var rsa1 = RSA.Create();
            rsa1.ImportParameters(parms);
            return x509.CopyWithPrivateKey(rsa1);
            
        }

        public static void SaveCertificateToPemAndKeyFiles(X509Certificate2 cert, string fileStem)
        {
            byte[] certificateBytes = cert.RawData;
            string certificatePem = CreatePemText("CERTIFICATE", certificateBytes);
            File.WriteAllText($"{fileStem}.pem", certificatePem);

            AsymmetricAlgorithm key = cert.GetRSAPrivateKey();
            //byte[] pubKeyBytes = key.ExportSubjectPublicKeyInfo();
            byte[] privKeyBytes = key.ExportPkcs8PrivateKey();
            //string pubKeyPem = CreatePemText("PUBLIC KEY", pubKeyBytes);
            string privKeyPem = CreatePemText("PRIVATE KEY", privKeyBytes);
            File.WriteAllText($"{fileStem}.key", privKeyPem);
        }

        public static string CreatePemText(string entityType, byte[] bytes)
        {
            StringBuilder builder = new StringBuilder();
            builder.AppendLine($"-----BEGIN {entityType}-----");
            builder.AppendLine(Convert.ToBase64String(bytes, Base64FormattingOptions.InsertLineBreaks));
            builder.AppendLine($"-----END {entityType}-----");

            return builder.ToString();
        }

        public static X509Certificate2 CreateFromPublicPrivateKey(string publicCert = "certs/public.pem", string privateCert = "certs/private.pem")
        {
            byte[] publicPemBytes = File.ReadAllBytes(publicCert);
            using var publicX509 = new X509Certificate2(publicPemBytes);
            var privateKeyText = File.ReadAllText(privateCert);
            var privateKeyBlocks = privateKeyText.Split("-", StringSplitOptions.RemoveEmptyEntries);
            var privateKeyBytes = Convert.FromBase64String(privateKeyBlocks[1]);

            using RSA rsa = RSA.Create();
            if (privateKeyBlocks[0] == "BEGIN PRIVATE KEY")
            {
                rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
            }
            else if (privateKeyBlocks[0] == "BEGIN RSA PRIVATE KEY")
            {
                rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
            }
            X509Certificate2 keyPair = publicX509.CopyWithPrivateKey(rsa);
            return keyPair;
        }
    }

FYI - immediately before you opened this, PR #3 had been merged which seems like a more concise approach

Thanks for the details. As mentioned, support for Windows has been added.