-
-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): add certificate generator command (#620)
BREAKING CHANGE: This allows the operator to create a valid selfsigned CA and server certificate ad-hoc in the cluster when using webhooks. Instead of generating the certificates locally and using them as config-map in kustomize, the operator can run the cli to generate the service certificate.
- Loading branch information
Showing
7 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
src/KubeOps.Cli/Commands/Generator/CertificateGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
using System.CommandLine; | ||
using System.CommandLine.Invocation; | ||
using System.Text; | ||
|
||
using KubeOps.Cli.Output; | ||
|
||
using Org.BouncyCastle.Asn1.X509; | ||
using Org.BouncyCastle.Crypto; | ||
using Org.BouncyCastle.Crypto.Generators; | ||
using Org.BouncyCastle.Crypto.Operators; | ||
using Org.BouncyCastle.Crypto.Prng; | ||
using Org.BouncyCastle.Math; | ||
using Org.BouncyCastle.OpenSsl; | ||
using Org.BouncyCastle.Security; | ||
using Org.BouncyCastle.Utilities; | ||
using Org.BouncyCastle.X509; | ||
using Org.BouncyCastle.X509.Extension; | ||
|
||
using Spectre.Console; | ||
|
||
namespace KubeOps.Cli.Commands.Generator; | ||
|
||
internal static class CertificateGenerator | ||
{ | ||
public static Command Command | ||
{ | ||
get | ||
{ | ||
var cmd = new Command("certificates", "Generates a CA and a server certificate.") | ||
{ | ||
Options.OutputPath, Arguments.CertificateServerName, Arguments.CertificateServerNamespace, | ||
}; | ||
cmd.AddAlias("cert"); | ||
cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx)); | ||
|
||
return cmd; | ||
} | ||
} | ||
|
||
internal static async Task Handler(IAnsiConsole console, InvocationContext ctx) | ||
{ | ||
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath); | ||
var result = new ResultOutput(console, OutputFormat.Plain); | ||
|
||
console.MarkupLine("Generate [cyan]CA[/] certificate and private key."); | ||
var (caCert, caKey) = CreateCaCertificate(); | ||
|
||
result.Add("ca.pem", ToPem(caCert)); | ||
result.Add("ca-key.pem", ToPem(caKey)); | ||
|
||
console.MarkupLine("Generate [cyan]server[/] certificate and private key."); | ||
var (srvCert, srvKey) = CreateServerCertificate( | ||
(caCert, caKey), | ||
ctx.ParseResult.GetValueForArgument(Arguments.CertificateServerName), | ||
ctx.ParseResult.GetValueForArgument(Arguments.CertificateServerNamespace)); | ||
|
||
result.Add("svc.pem", ToPem(srvCert)); | ||
result.Add("svc-key.pem", ToPem(srvKey)); | ||
|
||
if (outPath is not null) | ||
{ | ||
await result.Write(outPath); | ||
} | ||
else | ||
{ | ||
result.Write(); | ||
} | ||
} | ||
|
||
private static string ToPem(object obj) | ||
{ | ||
var sb = new StringBuilder(); | ||
using var writer = new PemWriter(new StringWriter(sb)); | ||
writer.WriteObject(obj); | ||
return sb.ToString(); | ||
} | ||
|
||
private static (X509Certificate Certificate, AsymmetricCipherKeyPair Key) CreateCaCertificate() | ||
{ | ||
var randomGenerator = new CryptoApiRandomGenerator(); | ||
var random = new SecureRandom(randomGenerator); | ||
|
||
// The Certificate Generator | ||
var certificateGenerator = new X509V3CertificateGenerator(); | ||
|
||
// Serial Number | ||
var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random); | ||
certificateGenerator.SetSerialNumber(serialNumber); | ||
|
||
// Issuer and Subject Name | ||
var name = new X509Name("CN=Operator Root CA, C=DEV, L=Kubernetes"); | ||
certificateGenerator.SetIssuerDN(name); | ||
certificateGenerator.SetSubjectDN(name); | ||
|
||
// Valid For | ||
var notBefore = DateTime.UtcNow.Date; | ||
var notAfter = notBefore.AddYears(5); | ||
certificateGenerator.SetNotBefore(notBefore); | ||
certificateGenerator.SetNotAfter(notAfter); | ||
|
||
// Cert Extensions | ||
certificateGenerator.AddExtension( | ||
X509Extensions.BasicConstraints, | ||
true, | ||
new BasicConstraints(true)); | ||
certificateGenerator.AddExtension( | ||
X509Extensions.KeyUsage, | ||
true, | ||
new KeyUsage(KeyUsage.KeyCertSign | KeyUsage.CrlSign | KeyUsage.KeyEncipherment)); | ||
|
||
// Subject Public Key | ||
const int keyStrength = 256; | ||
var keyGenerator = new ECKeyPairGenerator("ECDSA"); | ||
keyGenerator.Init(new KeyGenerationParameters(random, keyStrength)); | ||
var key = keyGenerator.GenerateKeyPair(); | ||
|
||
certificateGenerator.SetPublicKey(key.Public); | ||
|
||
var signatureFactory = new Asn1SignatureFactory("SHA512WITHECDSA", key.Private, random); | ||
var certificate = certificateGenerator.Generate(signatureFactory); | ||
|
||
return (certificate, key); | ||
} | ||
|
||
private static (X509Certificate Certificate, AsymmetricCipherKeyPair Key) CreateServerCertificate( | ||
(X509Certificate Certificate, AsymmetricCipherKeyPair Key) ca, string serverName, string serverNamespace) | ||
{ | ||
var randomGenerator = new CryptoApiRandomGenerator(); | ||
var random = new SecureRandom(randomGenerator); | ||
|
||
// The Certificate Generator | ||
var certificateGenerator = new X509V3CertificateGenerator(); | ||
|
||
// Serial Number | ||
var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random); | ||
certificateGenerator.SetSerialNumber(serialNumber); | ||
|
||
// Issuer and Subject Name | ||
certificateGenerator.SetIssuerDN(ca.Certificate.SubjectDN); | ||
certificateGenerator.SetSubjectDN(new X509Name("CN=Operator Service, C=DEV, L=Kubernetes")); | ||
|
||
// Valid For | ||
var notBefore = DateTime.UtcNow.Date; | ||
var notAfter = notBefore.AddYears(5); | ||
certificateGenerator.SetNotBefore(notBefore); | ||
certificateGenerator.SetNotAfter(notAfter); | ||
|
||
// Cert Extensions | ||
certificateGenerator.AddExtension( | ||
X509Extensions.BasicConstraints, | ||
false, | ||
new BasicConstraints(false)); | ||
certificateGenerator.AddExtension( | ||
X509Extensions.KeyUsage, | ||
true, | ||
new KeyUsage(KeyUsage.NonRepudiation | KeyUsage.KeyEncipherment | KeyUsage.DigitalSignature)); | ||
certificateGenerator.AddExtension( | ||
X509Extensions.ExtendedKeyUsage, | ||
false, | ||
new ExtendedKeyUsage(KeyPurposeID.id_kp_clientAuth, KeyPurposeID.id_kp_serverAuth)); | ||
certificateGenerator.AddExtension( | ||
X509Extensions.SubjectKeyIdentifier, | ||
false, | ||
new SubjectKeyIdentifierStructure(ca.Key.Public)); | ||
certificateGenerator.AddExtension( | ||
X509Extensions.AuthorityKeyIdentifier, | ||
false, | ||
new AuthorityKeyIdentifierStructure(ca.Certificate)); | ||
certificateGenerator.AddExtension( | ||
X509Extensions.SubjectAlternativeName, | ||
false, | ||
new GeneralNames(new[] | ||
{ | ||
new GeneralName(GeneralName.DnsName, $"{serverName}.{serverNamespace}.svc"), | ||
new GeneralName(GeneralName.DnsName, $"*.{serverNamespace}.svc"), | ||
new GeneralName(GeneralName.DnsName, "*.svc"), | ||
})); | ||
|
||
// Subject Public Key | ||
const int keyStrength = 256; | ||
var keyGenerator = new ECKeyPairGenerator("ECDSA"); | ||
keyGenerator.Init(new KeyGenerationParameters(random, keyStrength)); | ||
var key = keyGenerator.GenerateKeyPair(); | ||
|
||
certificateGenerator.SetPublicKey(key.Public); | ||
|
||
var signatureFactory = new Asn1SignatureFactory("SHA512WITHECDSA", ca.Key.Private, random); | ||
var certificate = certificateGenerator.Generate(signatureFactory); | ||
|
||
return (certificate, key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
test/KubeOps.Cli.Test/Generator/CertificateGenerator.Test.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
using System.CommandLine; | ||
using System.CommandLine.Invocation; | ||
|
||
using FluentAssertions; | ||
|
||
using KubeOps.Cli.Commands.Generator; | ||
|
||
using Org.BouncyCastle.Crypto; | ||
using Org.BouncyCastle.OpenSsl; | ||
using Org.BouncyCastle.X509; | ||
|
||
using Spectre.Console.Testing; | ||
|
||
namespace KubeOps.Cli.Test.Generator; | ||
|
||
public class CertificateGeneratorTest | ||
{ | ||
[Fact] | ||
public async Task Should_Execute() | ||
{ | ||
var console = new TestConsole(); | ||
|
||
var cmd = CertificateGenerator.Command; | ||
var ctx = new InvocationContext( | ||
cmd.Parse("server", "namespace")); | ||
|
||
await CertificateGenerator.Handler(console, ctx); | ||
|
||
ctx.ExitCode.Should().Be(ExitCodes.Success); | ||
} | ||
|
||
[Theory] | ||
[InlineData("ca.pem")] | ||
[InlineData("ca-key.pem")] | ||
[InlineData("svc.pem")] | ||
[InlineData("svc-key.pem")] | ||
public async Task Should_Generate_Certificate_Files(string file) | ||
{ | ||
var console = new TestConsole(); | ||
|
||
var cmd = CertificateGenerator.Command; | ||
var ctx = new InvocationContext( | ||
cmd.Parse("server", "namespace")); | ||
|
||
await CertificateGenerator.Handler(console, ctx); | ||
|
||
console.Output.Should().Contain($"File: {file}"); | ||
} | ||
|
||
[Fact] | ||
public async Task Should_Generate_Valid_Certificates() | ||
{ | ||
var console = new TestConsole(); | ||
|
||
var cmd = CertificateGenerator.Command; | ||
var ctx = new InvocationContext( | ||
cmd.Parse("server", "namespace")); | ||
|
||
await CertificateGenerator.Handler(console, ctx); | ||
|
||
var output = console.Lines.ToArray(); | ||
var caCertString = string.Join('\n', output[4..15]); | ||
var caCertKeyString = string.Join('\n', output[18..23]); | ||
var srvCertString = string.Join('\n', output[26..42]); | ||
var srvCertKeyString = string.Join('\n', output[45..50]); | ||
|
||
if (new PemReader(new StringReader(caCertString)).ReadObject() is not X509Certificate caCert) | ||
{ | ||
Assert.Fail("Could not parse CA certificate."); | ||
return; | ||
} | ||
|
||
if (new PemReader(new StringReader(caCertKeyString)).ReadObject() is not AsymmetricCipherKeyPair caKey) | ||
{ | ||
Assert.Fail("Could not parse CA private key."); | ||
return; | ||
} | ||
|
||
if (new PemReader(new StringReader(srvCertString)).ReadObject() is not X509Certificate srvCert) | ||
{ | ||
Assert.Fail("Could not parse server certificate."); | ||
return; | ||
} | ||
|
||
if (new PemReader(new StringReader(srvCertKeyString)).ReadObject() is not AsymmetricCipherKeyPair) | ||
{ | ||
Assert.Fail("Could not parse server private key."); | ||
return; | ||
} | ||
|
||
caCert.IsValidNow.Should().BeTrue(); | ||
caCert.Verify(caKey.Public); | ||
|
||
srvCert.IsValidNow.Should().BeTrue(); | ||
srvCert.Verify(caKey.Public); | ||
} | ||
} |