Skip to content

Commit 08a0982

Browse files
committed
cmd/incus: Add 'remote get-client-certificate' and 'get-client-token' commands
Signed-off-by: stoven2k17 <[email protected]>
1 parent abe90d2 commit 08a0982

File tree

1 file changed

+139
-1
lines changed

1 file changed

+139
-1
lines changed

cmd/incus/remote.go

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
44
"bufio"
5+
"crypto/sha256"
6+
"crypto/tls"
57
"crypto/x509"
68
"encoding/pem"
79
"errors"
@@ -14,9 +16,10 @@ import (
1416
"slices"
1517
"sort"
1618
"strings"
19+
"time"
1720

21+
"github.com/golang-jwt/jwt/v5"
1822
"github.com/spf13/cobra"
19-
2023
incus "github.com/lxc/incus/v6/client"
2124
cli "github.com/lxc/incus/v6/internal/cmd"
2225
"github.com/lxc/incus/v6/internal/i18n"
@@ -83,6 +86,14 @@ func (c *cmdRemote) Command() *cobra.Command {
8386
remoteSetURLCmd := cmdRemoteSetURL{global: c.global, remote: c}
8487
cmd.AddCommand(remoteSetURLCmd.Command())
8588

89+
// Get client certificate
90+
remoteGetClientCertificateCmd := cmdRemoteGetClientCertificate{global: c.global, remote: c}
91+
cmd.AddCommand(remoteGetClientCertificateCmd.Command())
92+
93+
// Get client token
94+
remoteGetClientTokenCmd := cmdRemoteGetClientToken{global: c.global, remote: c}
95+
cmd.AddCommand(remoteGetClientTokenCmd.Command())
96+
8697
// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
8798
cmd.Args = cobra.NoArgs
8899
cmd.Run = func(cmd *cobra.Command, _ []string) { _ = cmd.Usage() }
@@ -691,6 +702,133 @@ func (c *cmdRemoteGetDefault) Command() *cobra.Command {
691702
return cmd
692703
}
693704

705+
// Get client certificate.
706+
type cmdRemoteGetClientCertificate struct {
707+
global *cmdGlobal
708+
remote *cmdRemote
709+
}
710+
711+
// Command returns a cobra.Command for get-client-certificate.
712+
func (c *cmdRemoteGetClientCertificate) Command() *cobra.Command {
713+
cmd := &cobra.Command{}
714+
cmd.Use = usage("get-client-certificate")
715+
cmd.Short = i18n.G("Print the client certificate used by this Incus client")
716+
cmd.RunE = c.Run
717+
return cmd
718+
}
719+
720+
// Runs the get-client-certificate cmmand.
721+
func (c *cmdRemoteGetClientCertificate) Run(cmd *cobra.Command, args []string) error {
722+
conf, err := config.LoadConfig(os.Getenv("INCUS_CONF"))
723+
if err != nil {
724+
return fmt.Errorf("failed to load client config: %w", err)
725+
}
726+
727+
if !conf.HasClientCertificate() {
728+
fmt.Fprintf(os.Stderr, i18n.G("Generating a client certificate.") + "\n")
729+
err := conf.GenerateClientCertificate()
730+
if err != nil {
731+
return fmt.Errorf("failed to generate client certificate: %w", err)
732+
}
733+
}
734+
735+
certPath := conf.ConfigPath("client.crt")
736+
737+
content, err := os.ReadFile(certPath)
738+
if err != nil {
739+
return fmt.Errorf("failed to read certificate: %w", err)
740+
}
741+
742+
fmt.Println(string(content))
743+
return nil
744+
}
745+
746+
type cmdRemoteGetClientToken struct {
747+
global *cmdGlobal
748+
remote *cmdRemote
749+
}
750+
751+
// Command returns a cobra.Command for get-client-token.
752+
func (c *cmdRemoteGetClientToken) Command() *cobra.Command {
753+
cmd := &cobra.Command{}
754+
cmd.Use = usage("get-client-token")
755+
cmd.Short = i18n.G("Generate a client token derived from the client certificate")
756+
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
757+
`Generate a client trust token derived from the existing client certificate and private key.
758+
759+
This is useful for remote authentication workflows where a token is passed to another Incus server.`))
760+
cmd.RunE = c.Run
761+
return cmd
762+
}
763+
764+
// Run runs the get-client-token logic.
765+
func (c *cmdRemoteGetClientToken) Run(cmd *cobra.Command, args []string) error {
766+
conf, err := config.LoadConfig(os.Getenv("INCUS_CONF"))
767+
if err != nil {
768+
return fmt.Errorf("failed to load client config: %w", err)
769+
}
770+
771+
certPath := conf.ConfigPath("client.crt")
772+
keyPath := conf.ConfigPath("client.key")
773+
774+
if _, err := os.Stat(certPath); os.IsNotExist(err) {
775+
return fmt.Errorf("client certificate not found at %s", certPath)
776+
}
777+
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
778+
return fmt.Errorf("client key not found at %s", keyPath)
779+
}
780+
781+
cert, err := os.ReadFile(certPath)
782+
if err != nil {
783+
return fmt.Errorf("failed to read certificate: %w", err)
784+
}
785+
key, err := os.ReadFile(keyPath)
786+
if err != nil {
787+
return fmt.Errorf("failed to read private key: %w", err)
788+
}
789+
790+
token, err := generateClientToken(cert, key)
791+
if err != nil {
792+
return fmt.Errorf("failed to generate token: %w", err)
793+
}
794+
795+
fmt.Println(token)
796+
return nil
797+
}
798+
799+
// generateClientToken returns a signed JWT derived from the client certificate and key.
800+
func generateClientToken(certPEM, keyPEM []byte) (string, error) {
801+
keypair, err := tls.X509KeyPair(certPEM, keyPEM)
802+
if err != nil {
803+
return "", err
804+
}
805+
806+
// Use SHA-256 fingerprint of the first cert in the chain
807+
fingerprint := sha256.Sum256(keypair.Certificate[0])
808+
subject := fmt.Sprintf("%x", fingerprint)
809+
810+
now := time.Now()
811+
claims := jwt.RegisteredClaims{
812+
Subject: subject,
813+
IssuedAt: jwt.NewNumericDate(now),
814+
NotBefore: jwt.NewNumericDate(now),
815+
ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
816+
}
817+
818+
// Trying signing with both ES384 and RS256
819+
algos := []jwt.SigningMethod{jwt.SigningMethodES384, jwt.SigningMethodRS256}
820+
821+
for _, alg := range algos {
822+
token := jwt.NewWithClaims(alg, claims)
823+
tokenStr, err := token.SignedString(keypair.PrivateKey)
824+
if err == nil {
825+
return tokenStr, nil
826+
}
827+
}
828+
829+
return "", fmt.Errorf("unable to sign JWT with available key algorithms")
830+
}
831+
694832
// Run is used in the RunE field of the cobra.Command returned by Command.
695833
func (c *cmdRemoteGetDefault) Run(cmd *cobra.Command, args []string) error {
696834
conf := c.global.conf

0 commit comments

Comments
 (0)