Skip to content
Open
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
5 changes: 5 additions & 0 deletions internal/cmd/skupper/common/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ type CommandVersionFlags struct {
type CommandDebugFlags struct {
}

type CommandDebugCertFlags struct {
Output string
File string
}

type CommandConnSweeperFlags struct {
IdleThreshold int
Execute bool
Expand Down
173 changes: 173 additions & 0 deletions internal/cmd/skupper/debug/cert/display.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cert

import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"os"
"strings"
"text/tabwriter"
"time"

"github.com/skupperproject/skupper/internal/certs"
"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
)

// Info holds the decoded fields of an X.509 certificate for display.
type Info struct {
Name string `json:"name"`
Subject string `json:"subject"`
Issuer string `json:"issuer"`
SerialNumber string `json:"serialNumber"`
NotBefore string `json:"notBefore"`
NotAfter string `json:"notAfter"`
DNSNames []string `json:"dnsNames,omitempty"`
IPAddresses []string `json:"ipAddresses,omitempty"`
EmailAddresses []string `json:"emailAddresses,omitempty"`
URIs []string `json:"uris,omitempty"`
IsCA bool `json:"isCA"`
IsSigningCert *bool `json:"isSigningCert,omitempty"`
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
PublicKeySize int `json:"publicKeySize"`
SignatureAlgorithm string `json:"signatureAlgorithm"`
Status string `json:"status,omitempty"`
CrExpiration string `json:"crExpiration,omitempty"`
}

func ParseCertificate(name string, certData []byte) (*Info, error) {
cert, err := certs.DecodeCertificate(certData)
if err != nil {
return nil, err
}
return certToInfo(name, cert), nil
}

func ParseCertificateFile(name, path string) (*Info, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseCertificate(name, data)
}

func certToInfo(name string, cert *x509.Certificate) *Info {
algo, size := publicKeyInfo(cert)
ips := make([]string, len(cert.IPAddresses))
for i, ip := range cert.IPAddresses {
ips[i] = ip.String()
}
uris := make([]string, len(cert.URIs))
for i, uri := range cert.URIs {
uris[i] = uri.String()
}
return &Info{
Name: name,
Subject: formatName(cert.Subject),
Issuer: formatName(cert.Issuer),
SerialNumber: cert.SerialNumber.String(),
NotBefore: cert.NotBefore.Format(time.RFC3339),
NotAfter: cert.NotAfter.Format(time.RFC3339),
DNSNames: cert.DNSNames,
IPAddresses: ips,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
EmailAddresses: cert.EmailAddresses,
URIs: uris,
IsCA: cert.IsCA,
PublicKeyAlgorithm: algo,
PublicKeySize: size,
SignatureAlgorithm: cert.SignatureAlgorithm.String(),
}
}

func formatName(name pkix.Name) string {
if name.CommonName != "" {
return name.CommonName
}
return name.String()
}

func publicKeyInfo(cert *x509.Certificate) (algorithm string, size int) {
switch pub := cert.PublicKey.(type) {
case *rsa.PublicKey:
return "RSA", pub.N.BitLen()
case *ecdsa.PublicKey:
return "ECDSA", pub.Curve.Params().BitSize
default:
return fmt.Sprintf("%T", cert.PublicKey), 0
}
}

func formatSANs(info *Info) string {
var parts []string
parts = append(parts, info.DNSNames...)
parts = append(parts, info.IPAddresses...)
parts = append(parts, info.EmailAddresses...)
parts = append(parts, info.URIs...)
if len(parts) == 0 {
return ""
}
return strings.Join(parts, ", ")
}

// Display renders one or more certificates to stdout.
func Display(infos []Info, output string, detail bool) error {
if output != "" {
for _, info := range infos {
encoded, err := utils.Encode(output, info)
if err != nil {
return err
}
fmt.Println(encoded)
}
return nil
}
if detail && len(infos) == 1 {
displayDetail(infos[0])
return nil
}
return displayList(infos)
}

func displayDetail(info Info) {
fmt.Printf("Name\t\t: %s\n", info.Name)
fmt.Printf("Subject\t\t: %s\n", info.Subject)
fmt.Printf("Issuer\t\t: %s\n", info.Issuer)
fmt.Printf("Serial Number\t: %s\n", info.SerialNumber)
fmt.Printf("Not Before\t: %s\n", info.NotBefore)
fmt.Printf("Not After\t: %s\n", info.NotAfter)
fmt.Printf("Is CA\t\t: %t\n", info.IsCA)
if info.IsSigningCert != nil {
fmt.Printf("Is Signing Cert\t: %t\n", *info.IsSigningCert)
}
fmt.Printf("SANs\t\t: %s\n", formatSANs(&info))
if info.PublicKeySize > 0 {
fmt.Printf("Public Key\t: %s (%d bits)\n", info.PublicKeyAlgorithm, info.PublicKeySize)
} else {
fmt.Printf("Public Key\t: %s\n", info.PublicKeyAlgorithm)
}
fmt.Printf("Signature\t: %s\n", info.SignatureAlgorithm)
if info.Status != "" {
fmt.Printf("Status\t\t: %s\n", info.Status)
}
if info.CrExpiration != "" {
fmt.Printf("CR Expiration\t: %s\n", info.CrExpiration)
}
}

func displayList(infos []Info) error {
if len(infos) == 0 {
fmt.Println("No certificates found")
return nil
}
writer := tabwriter.NewWriter(os.Stdout, 0, 8, 2, '\t', 0)
fmt.Fprintln(writer, "NAME\tSUBJECT\tISSUER\tNOT AFTER\tSANs")
for _, info := range infos {
sans := formatSANs(&info)
if len(sans) > 60 {
sans = sans[:57] + "..."
}
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", info.Name, info.Subject, info.Issuer, info.NotAfter, sans)
}
return writer.Flush()
}
33 changes: 33 additions & 0 deletions internal/cmd/skupper/debug/cert/display_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cert

import (
"testing"

"github.com/skupperproject/skupper/internal/certs"
"gotest.tools/v3/assert"
)

func TestParseCertificate(t *testing.T) {
caSecret, err := certs.GenerateSecret("test-ca", "test-ca.example.com", nil, 0, nil)
assert.NilError(t, err)

leafSecret, err := certs.GenerateSecret("test-cert", "test.example.com", []string{"test.example.com"}, 86400000000000, caSecret)
assert.NilError(t, err)

info, err := ParseCertificate("test-cert", leafSecret.Data["tls.crt"])
assert.NilError(t, err)

assert.Equal(t, "test-cert", info.Name)
assert.Equal(t, "test.example.com", info.Subject)
assert.Equal(t, "test-ca.example.com", info.Issuer)
assert.Equal(t, "RSA", info.PublicKeyAlgorithm)
assert.Equal(t, 2048, info.PublicKeySize)
assert.Assert(t, len(info.NotBefore) > 0)
assert.Assert(t, len(info.NotAfter) > 0)
assert.DeepEqual(t, []string{"test.example.com"}, info.DNSNames)
}

func TestParseCertificateInvalidPEM(t *testing.T) {
_, err := ParseCertificate("bad", []byte("not a pem"))
assert.ErrorContains(t, err, "Could not decode PEM block")
}
33 changes: 33 additions & 0 deletions internal/cmd/skupper/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func NewCmdDebug() *cobra.Command {
}
platform := common.Platform(config.GetPlatform())
cmd.AddCommand(CmdDebugDumpFactory(platform))
cmd.AddCommand(CmdDebugCertFactory(platform))
cmd.AddCommand(CmdDebugSweepFactory(platform))

return cmd
Expand Down Expand Up @@ -64,6 +65,38 @@ skupper debug sweep --port 8080 --port 9090 --idle-threshold 14400 --execute`,
return cmd
}

func CmdDebugCertFactory(configuredPlatform common.Platform) *cobra.Command {
kubeCommand := kube.NewCmdDebugCert()
nonKubeCommand := nonkube.NewCmdDebugCert()

cmdDesc := common.SkupperCmdDescription{
Use: "cert-inspect [name]",
Short: "Inspect X.509 certificates in use by Skupper",
Long: `Decode and display key fields of X.509 certificates managed by Skupper,
including subject, issuer, validity period, SANs, and public key information.

Without a name, all certificates in the namespace are listed. With a name,
detailed information for that certificate is shown.`,
Example: `skupper debug cert-inspect
skupper debug cert-inspect skupper-local-server
skupper debug cert-inspect --file /path/to/tls.crt
skupper debug cert-inspect -o yaml`,
}

cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDesc, kubeCommand, nonKubeCommand)

cmdFlags := common.CommandDebugCertFlags{}
cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
cmd.Flags().StringVar(&cmdFlags.File, "file", "", "Inspect a local PEM-encoded certificate file")

kubeCommand.CobraCmd = cmd
kubeCommand.Flags = &cmdFlags
nonKubeCommand.CobraCmd = cmd
nonKubeCommand.Flags = &cmdFlags

return cmd
}

func CmdDebugDumpFactory(configuredPlatform common.Platform) *cobra.Command {
kubeCommand := kube.NewCmdDebug()
nonKubeCommand := nonkube.NewCmdDebug()
Expand Down
8 changes: 8 additions & 0 deletions internal/cmd/skupper/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ func TestCmdLinkFactory(t *testing.T) {
expectedFlagsWithDefaultValue: map[string]interface{}{},
command: CmdDebugDumpFactory(common.PlatformKubernetes),
},
{
name: "CmdDebugCertFactory",
expectedFlagsWithDefaultValue: map[string]interface{}{
"output": "",
"file": "",
},
command: CmdDebugCertFactory(common.PlatformKubernetes),
},
}

for _, test := range testTable {
Expand Down
Loading
Loading