@@ -2,6 +2,8 @@ package main
2
2
3
3
import (
4
4
"bufio"
5
+ "crypto/sha256"
6
+ "crypto/tls"
5
7
"crypto/x509"
6
8
"encoding/pem"
7
9
"errors"
@@ -14,9 +16,10 @@ import (
14
16
"slices"
15
17
"sort"
16
18
"strings"
19
+ "time"
17
20
21
+ "github.com/golang-jwt/jwt/v5"
18
22
"github.com/spf13/cobra"
19
-
20
23
incus "github.com/lxc/incus/v6/client"
21
24
cli "github.com/lxc/incus/v6/internal/cmd"
22
25
"github.com/lxc/incus/v6/internal/i18n"
@@ -83,6 +86,14 @@ func (c *cmdRemote) Command() *cobra.Command {
83
86
remoteSetURLCmd := cmdRemoteSetURL {global : c .global , remote : c }
84
87
cmd .AddCommand (remoteSetURLCmd .Command ())
85
88
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
+
86
97
// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
87
98
cmd .Args = cobra .NoArgs
88
99
cmd .Run = func (cmd * cobra.Command , _ []string ) { _ = cmd .Usage () }
@@ -691,6 +702,133 @@ func (c *cmdRemoteGetDefault) Command() *cobra.Command {
691
702
return cmd
692
703
}
693
704
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
+
694
832
// Run is used in the RunE field of the cobra.Command returned by Command.
695
833
func (c * cmdRemoteGetDefault ) Run (cmd * cobra.Command , args []string ) error {
696
834
conf := c .global .conf
0 commit comments