From ac9954978833c72931f59b1a7f0ae35507875acd Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Wed, 19 Jul 2023 13:16:52 -0500 Subject: [PATCH 1/8] Add Namespace Management CLI Commands This commit adds several new commands to the CLI for managing namespaces. The commands added include: - Register a new namespace - Delete a namespace - List all namespaces - Get a specific namespace This commit also adds the necessary flag definitions and configurations for each command. In addition, the commit includes the implementation of JWT-based authentication when registering a namespace. This includes handling of JSON Web Key Sets (JWKS), signing/verification of payloads, nonce generation, and HTTP request/response handling. The code also includes functions for loading and saving signatures to/from a file, and for loading ECDSA public/private keys from PEM and JWKS formats. --- cmd/namespace.go | 94 +++++++++++++++ cmd/namespace_registry.go | 247 ++++++++++++++++++++++++++++++++++++++ cmd/root.go | 9 +- 3 files changed, 344 insertions(+), 6 deletions(-) create mode 100644 cmd/namespace.go create mode 100644 cmd/namespace_registry.go diff --git a/cmd/namespace.go b/cmd/namespace.go new file mode 100644 index 000000000..ef6259cdf --- /dev/null +++ b/cmd/namespace.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// These functions are just placeholders. You need to provide actual implementation. + +var withIdentity bool +var prefix string +var host string +var jwks bool +var pubkeyPath string +var privkeyPath string + +func registerANamespace(cmd *cobra.Command, args []string) { + endpoint := host + "/registry" + if prefix == "" { + fmt.Println("Error: prefix is required") + return + } + + if withIdentity { + namespace_register_with_identity(pubkeyPath, privkeyPath, endpoint, prefix) + } else { + namespace_register(pubkeyPath, privkeyPath, endpoint, "", prefix) + } +} + +func deleteANamespace(cmd *cobra.Command, args []string) { + endpoint := host + "/" + prefix + delete_namespace(endpoint) +} + +func listAllNamespaces(cmd *cobra.Command, args []string) { + endpoint := host + list_namespaces(endpoint) +} + +func getNamespace(cmd *cobra.Command, args []string) { + if jwks { + endpoint := host + "/" + prefix + "/issuer.jwks" + get_namespace(endpoint) + } else { + fmt.Println("Get command is not yet implemented.") + } +} + +var namespaceCmd = &cobra.Command{ + Use: "namespace", + Short: "Work with namespaces", +} + +var registerCmd = &cobra.Command{ + Use: "register", + Short: "Register a new namespace", + Run: registerANamespace, +} + +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a namespace", + Run: deleteANamespace, +} + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List all namespaces", + Run: listAllNamespaces, +} + +var getCmd = &cobra.Command{ + Use: "get", + Short: "Get a specific namespace", + Run: getNamespace, +} + +func init() { + registerCmd.Flags().StringVar(&prefix, "prefix", "", "prefix for registering namespace") + registerCmd.Flags().BoolVar(&withIdentity, "with-identity", false, "Register a namespace with an identity") + getCmd.Flags().StringVar(&prefix, "prefix", "", "prefix for get namespace") + getCmd.Flags().BoolVar(&jwks, "jwks", false, "Get the jwks of the namespace") + deleteCmd.Flags().StringVar(&prefix, "prefix", "", "prefix for delete namespace") + + namespaceCmd.PersistentFlags().StringVar(&host, "host", "http://localhost:8443/cli-namespaces", "Host of the namespace registry") + namespaceCmd.PersistentFlags().StringVar(&pubkeyPath, "pubkey", "/usr/src/app/pelican/cmd/cert/.well-known/client.jwks", "Path to the public key") + namespaceCmd.PersistentFlags().StringVar(&privkeyPath, "privkey", "/usr/src/app/pelican/cmd/cert/client.key", "Path to the private key") + namespaceCmd.AddCommand(registerCmd) + namespaceCmd.AddCommand(deleteCmd) + namespaceCmd.AddCommand(listCmd) + namespaceCmd.AddCommand(getCmd) +} diff --git a/cmd/namespace_registry.go b/cmd/namespace_registry.go new file mode 100644 index 000000000..3ab389664 --- /dev/null +++ b/cmd/namespace_registry.go @@ -0,0 +1,247 @@ +package main + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/elliptic" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "math/big" + "net/http" + "bytes" + "bufio" +) + +type JWKS struct { + Keys []JWK `json:"keys"` +} + +type JWK struct { + Crv string `json:"crv"` + Kid string `json:"kid"` + Kty string `json:"kty"` + X string `json:"x"` + Y string `json:"y"` +} + +func loadPrivateKey(privateKeyPath string) (*ecdsa.PrivateKey, error) { + keyInBytes, err := ioutil.ReadFile(privateKeyPath) + if err != nil { + return nil, err + } + block, _ := pem.Decode(keyInBytes) + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return privateKey.(*ecdsa.PrivateKey), nil +} + +func loadPublicKey(publicKeyPath string) (*ecdsa.PublicKey, error) { + keyInBytes, err := ioutil.ReadFile(publicKeyPath) + if err != nil { + return nil, err + } + var jwks JWKS + err = json.Unmarshal(keyInBytes, &jwks) + if err != nil { + return nil, err + } + keyData := jwks.Keys[0] // Assumes there's at least one key + xBytes, _ := base64.RawURLEncoding.DecodeString(keyData.X) + yBytes, _ := base64.RawURLEncoding.DecodeString(keyData.Y) + x := new(big.Int).SetBytes(xBytes) + y := new(big.Int).SetBytes(yBytes) + publicKey := &ecdsa.PublicKey{ + Curve: elliptic.P521(), + X: x, + Y: y, + } + return publicKey, nil +} + + +func signPayload(payload []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) { + hash := sha256.Sum256(payload) + signature, err := privateKey.Sign(rand.Reader, hash[:], crypto.SHA256) // Use crypto.SHA256 instead of the hash[:] + if err != nil { + return nil, err + } + return signature, nil +} + +func writeSignatureToFile(signature []byte, filename string) error { + err := ioutil.WriteFile(filename, signature, 0644) + if err != nil { + return err + } + return nil +} + +func loadSignatureFromFile(filename string) ([]byte, error) { + signature, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return signature, nil +} + +func verifySignature(payload []byte, signature []byte, publicKey *ecdsa.PublicKey) bool { + hash := sha256.Sum256(payload) + return ecdsa.VerifyASN1(publicKey, hash[:], signature) +} + +func generateNonce() (string, error) { + nonce := make([]byte, 32) + _, err := rand.Read(nonce) + if err != nil { + return "", err + } + return hex.EncodeToString(nonce), nil +} + +func make_request(url string, method string, data map[string]interface{}) ([]byte) { + payload, _ := json.Marshal(data) + + req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + return body +} + +func resp_to_json(body []byte) (map[string]string) { + // Unmarshal the response body + var respData map[string]string + err := json.Unmarshal(body, &respData) + if err != nil { + panic(err) + } + return respData +} + +func namespace_register_with_identity(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, prefix string) () { + data := map[string]interface{}{ + "identity_required": "true", + } + resp := make_request(namespaceRegistryEndpoint, "POST", data) + respData := resp_to_json(resp) + + verification_url := respData["verification_url"] + device_code := respData["device_code"] + fmt.Printf("Verification URL: %s\n", verification_url) + + done := false + for !done { + data = map[string]interface{}{ + "identity_required": "true", + "device_code": device_code, + } + resp = make_request(namespaceRegistryEndpoint, "POST", data) + respData = resp_to_json(resp) + + if respData["status"] == "APPROVED" { + done = true + } else { + fmt.Printf("Waiting for approval...\n") + reader := bufio.NewReader(os.Stdin) + fmt.Print("Press Enter after verification") + _, _ = reader.ReadString('\n') + // time.Sleep(60 * time.Second) + } + } + access_token := respData["access_token"] + fmt.Printf("Access token: %s\n", access_token) + namespace_register(publicKeyPath, privateKeyPath, namespaceRegistryEndpoint, access_token, prefix) + +} + +func namespace_register(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, access_token string, prefix string) () { + publicKey, err := loadPublicKey(publicKeyPath) + if err != nil { + fmt.Printf("Failed to load public key: %v\n", err) + os.Exit(1) + } + + privateKey, err := loadPrivateKey(privateKeyPath) + if err != nil { + fmt.Printf("Failed to load private key: %v\n", err) + os.Exit(1) + } + + client_nonce, err := generateNonce() + if err != nil { + fmt.Printf("Failed to generate nonce: %v\n", err) + os.Exit(1) + } + + data := map[string]interface{}{ + "client_nonce": client_nonce, + "pubkey": fmt.Sprintf("%x", publicKey.X.Bytes()), + } + + resp := make_request(namespaceRegistryEndpoint, "POST", data) + respData := resp_to_json(resp) + + // Create client payload by concatenating client_nonce and server_nonce + clientPayload := client_nonce + respData["server_nonce"] + + // Sign the payload + signature, err := signPayload([]byte(clientPayload), privateKey) + if err != nil { + panic(err) + } + + // Create data for the second POST request + data2 := map[string]interface{}{ + "client_nonce": client_nonce, + "server_nonce": respData["server_nonce"], + "pubkey": map[string]string{ + "x": publicKey.X.String(), + "y": publicKey.Y.String(), + "curve": "P-521", + }, + "client_payload": clientPayload, + "client_signature": hex.EncodeToString(signature), + "server_payload": respData["server_payload"], + "server_signature": respData["server_signature"], + "prefix": prefix, + "access_token": access_token, + } + + // Send the second POST request + make_request(namespaceRegistryEndpoint, "POST", data2) + fmt.Printf("Namespace registered successfully\n") +} + +func list_namespaces(endpoint string) { + respData := make_request(endpoint, "GET", nil) + fmt.Println(string(respData)) +} + +func get_namespace(endpoint string) { + respData := make_request(endpoint, "GET", nil) + fmt.Println(string(respData)) +} + +func delete_namespace(endpoint string) { + respData := make_request(endpoint, "DELETE", nil) + fmt.Println(string(respData)) +} + + diff --git a/cmd/root.go b/cmd/root.go index 92dd5daed..a8410c8a7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,6 +45,7 @@ func init() { rootCmd.AddCommand(objectCmd) objectCmd.CompletionOptions.DisableDefaultCmd = true rootCmd.AddCommand(originCmd) + rootCmd.AddCommand(namespaceCmd) rootCmd.AddCommand(rootConfigCmd) rootCmd.AddCommand(rootPluginCmd) preferredPrefix := config.GetPreferredPrefix() @@ -55,9 +56,7 @@ func init() { rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug logs") rootCmd.PersistentFlags().StringP("federation", "f", "", "Pelican federation to utilize") - if err := viper.BindPFlag("FederationURL", rootCmd.PersistentFlags().Lookup("federation")); err != nil { - panic(err) - } + viper.BindPFlag("FederationURL", rootCmd.PersistentFlags().Lookup("federation")) rootCmd.PersistentFlags().BoolVarP(&outputJSON, "json", "", false, "output results in JSON format") rootCmd.CompletionOptions.DisableDefaultCmd = true @@ -75,9 +74,7 @@ func initConfig() { viper.SetConfigType("yaml") viper.SetConfigName("pelican.yaml") } - if err := viper.BindPFlag("Debug", rootCmd.PersistentFlags().Lookup("debug")); err != nil { - panic(err) - } + viper.BindPFlag("Debug", rootCmd.Flags().Lookup("debug")) viper.SetEnvPrefix(config.GetPreferredPrefix()) viper.AutomaticEnv() From 8a1b70832ab5c040f0e646d36c8a2c152075b0fb Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Mon, 24 Jul 2023 10:06:58 -0500 Subject: [PATCH 2/8] Handle error returns and comment out unused functions - Checked error returns from 'viper.BindPFlag' calls for robust error handling. - Commented out unused functions 'writeSignatureToFile', 'loadSignatureFromFile', 'verifySignature' to prevent 'unused' linting errors. - Utilized the err variable after client.Do(req) call to prevent ineffectual assignment. --- cmd/namespace_registry.go | 6 ++++++ cmd/root.go | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/namespace_registry.go b/cmd/namespace_registry.go index 3ab389664..76064ae47 100644 --- a/cmd/namespace_registry.go +++ b/cmd/namespace_registry.go @@ -78,6 +78,7 @@ func signPayload(payload []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) { return signature, nil } +/* func writeSignatureToFile(signature []byte, filename string) error { err := ioutil.WriteFile(filename, signature, 0644) if err != nil { @@ -98,6 +99,7 @@ func verifySignature(payload []byte, signature []byte, publicKey *ecdsa.PublicKe hash := sha256.Sum256(payload) return ecdsa.VerifyASN1(publicKey, hash[:], signature) } +*/ func generateNonce() (string, error) { nonce := make([]byte, 32) @@ -112,6 +114,10 @@ func make_request(url string, method string, data map[string]interface{}) ([]byt payload, _ := json.Marshal(data) req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") client := &http.Client{} diff --git a/cmd/root.go b/cmd/root.go index a8410c8a7..4f58f5bf3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,7 +56,11 @@ func init() { rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug logs") rootCmd.PersistentFlags().StringP("federation", "f", "", "Pelican federation to utilize") - viper.BindPFlag("FederationURL", rootCmd.PersistentFlags().Lookup("federation")) + err := viper.BindPFlag("FederationURL", rootCmd.PersistentFlags().Lookup("federation")) + + if err != nil { + log.Fatalf("Error binding flag: %v", err) + } rootCmd.PersistentFlags().BoolVarP(&outputJSON, "json", "", false, "output results in JSON format") rootCmd.CompletionOptions.DisableDefaultCmd = true From 27eb1bf0bb6a5b15a0afec23a55b8c778be19c59 Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Mon, 24 Jul 2023 10:13:18 -0500 Subject: [PATCH 3/8] Checked error returns in root.go from 'viper.BindPFlag' calls for robust error handling. --- cmd/origin.go | 3 +-- cmd/root.go | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/origin.go b/cmd/origin.go index 1a68d7a38..78871c8cc 100644 --- a/cmd/origin.go +++ b/cmd/origin.go @@ -38,8 +38,7 @@ func init() { originCmd.AddCommand(originConfigCmd) originCmd.AddCommand(originServeCmd) originServeCmd.Flags().StringP("volume", "v", "", "Setting the volue to /SRC:/DEST will export the contents of /SRC as /DEST in the Pelican federation") - err := viper.BindPFlag("ExportVolume", originServeCmd.Flags().Lookup("volume")) - if err != nil { + if err := viper.BindPFlag("ExportVolume", originServeCmd.Flags().Lookup("volume")); err != nil { panic(err) } } diff --git a/cmd/root.go b/cmd/root.go index 4f58f5bf3..63d2760e0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,10 +56,8 @@ func init() { rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug logs") rootCmd.PersistentFlags().StringP("federation", "f", "", "Pelican federation to utilize") - err := viper.BindPFlag("FederationURL", rootCmd.PersistentFlags().Lookup("federation")) - - if err != nil { - log.Fatalf("Error binding flag: %v", err) + if err := viper.BindPFlag("FederationURL", rootCmd.PersistentFlags().Lookup("federation")); err != nil { + panic(err) } rootCmd.PersistentFlags().BoolVarP(&outputJSON, "json", "", false, "output results in JSON format") @@ -78,7 +76,9 @@ func initConfig() { viper.SetConfigType("yaml") viper.SetConfigName("pelican.yaml") } - viper.BindPFlag("Debug", rootCmd.Flags().Lookup("debug")) + if err := viper.BindPFlag("Debug", rootCmd.Flags().Lookup("debug")); err != nil { + panic(err) + } viper.SetEnvPrefix(config.GetPreferredPrefix()) viper.AutomaticEnv() From 4e58e0d1c5a38778b9a03d1707369b8e3e68bfb5 Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Mon, 24 Jul 2023 10:28:41 -0500 Subject: [PATCH 4/8] fix a type with handling flag debug --- cmd/root.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 63d2760e0..0507cd1b3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -37,9 +37,6 @@ func Execute() { } func init() { - - - cobra.OnInitialize(initConfig) rootCmd.AddCommand(objectCmd) From cf7c1798f4d8289153681c97ae07c6112878b000 Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Mon, 24 Jul 2023 10:50:14 -0500 Subject: [PATCH 5/8] resolve a conflit in root.go --- cmd/root.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 0507cd1b3..8cfd12973 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -37,6 +37,9 @@ func Execute() { } func init() { + + + cobra.OnInitialize(initConfig) rootCmd.AddCommand(objectCmd) @@ -73,7 +76,7 @@ func initConfig() { viper.SetConfigType("yaml") viper.SetConfigName("pelican.yaml") } - if err := viper.BindPFlag("Debug", rootCmd.Flags().Lookup("debug")); err != nil { + if err := viper.BindPFlag("Debug", rootCmd.PersistentFlags().Lookup("debug")); err != nil { panic(err) } @@ -89,4 +92,4 @@ func initConfig() { if viper.GetBool("Debug") { setLogging(log.DebugLevel) } -} +} \ No newline at end of file From 6bba9a7c8c379269c422e9d99dc36eb385c1046a Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Mon, 31 Jul 2023 08:46:42 -0500 Subject: [PATCH 6/8] 1. refactor cred-related code into `config/init_server_creds.go` 2. clean code 3. avoid panic'ing within the CLI --- cmd/namespace.go | 40 ++++++-- cmd/namespace_registry.go | 177 +++++++++++++----------------------- config/init_server_creds.go | 153 +++++++++++++++++++------------ 3 files changed, 193 insertions(+), 177 deletions(-) diff --git a/cmd/namespace.go b/cmd/namespace.go index ef6259cdf..c2d74b76b 100644 --- a/cmd/namespace.go +++ b/cmd/namespace.go @@ -1,9 +1,10 @@ package main import ( - "fmt" + "os" "github.com/spf13/cobra" + log "github.com/sirupsen/logrus" ) // These functions are just placeholders. You need to provide actual implementation. @@ -18,33 +19,54 @@ var privkeyPath string func registerANamespace(cmd *cobra.Command, args []string) { endpoint := host + "/registry" if prefix == "" { - fmt.Println("Error: prefix is required") - return + log.Error("Error: prefix is required") + os.Exit(1) } if withIdentity { - namespace_register_with_identity(pubkeyPath, privkeyPath, endpoint, prefix) + err := namespace_register_with_identity(pubkeyPath, privkeyPath, endpoint, prefix) + if err != nil { + log.Error(err) + os.Exit(1) + } } else { - namespace_register(pubkeyPath, privkeyPath, endpoint, "", prefix) + err := namespace_register(pubkeyPath, privkeyPath, endpoint, "", prefix) + if err != nil { + log.Error(err) + os.Exit(1) + } } } func deleteANamespace(cmd *cobra.Command, args []string) { endpoint := host + "/" + prefix - delete_namespace(endpoint) + err := delete_namespace(endpoint) + if err != nil { + log.Error(err) + os.Exit(1) + } } func listAllNamespaces(cmd *cobra.Command, args []string) { endpoint := host - list_namespaces(endpoint) + err := list_namespaces(endpoint) + if err != nil { + log.Error(err) + os.Exit(1) + } } func getNamespace(cmd *cobra.Command, args []string) { if jwks { endpoint := host + "/" + prefix + "/issuer.jwks" - get_namespace(endpoint) + err := get_namespace(endpoint) + if err != nil { + log.Error(err) + os.Exit(1) + } } else { - fmt.Println("Get command is not yet implemented.") + log.Error("Error: get command requires --jwks flag") + os.Exit(1) } } diff --git a/cmd/namespace_registry.go b/cmd/namespace_registry.go index 76064ae47..703af373b 100644 --- a/cmd/namespace_registry.go +++ b/cmd/namespace_registry.go @@ -5,68 +5,19 @@ import ( "crypto/ecdsa" "crypto/rand" "crypto/sha256" - "crypto/x509" - "crypto/elliptic" - "encoding/base64" "encoding/hex" "encoding/json" - "encoding/pem" "fmt" "io/ioutil" "os" - "math/big" "net/http" "bytes" "bufio" -) - -type JWKS struct { - Keys []JWK `json:"keys"` -} - -type JWK struct { - Crv string `json:"crv"` - Kid string `json:"kid"` - Kty string `json:"kty"` - X string `json:"x"` - Y string `json:"y"` -} - -func loadPrivateKey(privateKeyPath string) (*ecdsa.PrivateKey, error) { - keyInBytes, err := ioutil.ReadFile(privateKeyPath) - if err != nil { - return nil, err - } - block, _ := pem.Decode(keyInBytes) - privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - return privateKey.(*ecdsa.PrivateKey), nil -} + "math/big" + "encoding/base64" -func loadPublicKey(publicKeyPath string) (*ecdsa.PublicKey, error) { - keyInBytes, err := ioutil.ReadFile(publicKeyPath) - if err != nil { - return nil, err - } - var jwks JWKS - err = json.Unmarshal(keyInBytes, &jwks) - if err != nil { - return nil, err - } - keyData := jwks.Keys[0] // Assumes there's at least one key - xBytes, _ := base64.RawURLEncoding.DecodeString(keyData.X) - yBytes, _ := base64.RawURLEncoding.DecodeString(keyData.Y) - x := new(big.Int).SetBytes(xBytes) - y := new(big.Int).SetBytes(yBytes) - publicKey := &ecdsa.PublicKey{ - Curve: elliptic.P521(), - X: x, - Y: y, - } - return publicKey, nil -} + "github.com/pelicanplatform/pelican/config" +) func signPayload(payload []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) { @@ -78,29 +29,6 @@ func signPayload(payload []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) { return signature, nil } -/* -func writeSignatureToFile(signature []byte, filename string) error { - err := ioutil.WriteFile(filename, signature, 0644) - if err != nil { - return err - } - return nil -} - -func loadSignatureFromFile(filename string) ([]byte, error) { - signature, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - return signature, nil -} - -func verifySignature(payload []byte, signature []byte, publicKey *ecdsa.PublicKey) bool { - hash := sha256.Sum256(payload) - return ecdsa.VerifyASN1(publicKey, hash[:], signature) -} -*/ - func generateNonce() (string, error) { nonce := make([]byte, 32) _, err := rand.Read(nonce) @@ -110,12 +38,12 @@ func generateNonce() (string, error) { return hex.EncodeToString(nonce), nil } -func make_request(url string, method string, data map[string]interface{}) ([]byte) { +func make_request(url string, method string, data map[string]interface{}) ([]byte, error) { payload, _ := json.Marshal(data) req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) if err != nil { - panic(err) + return nil, err } req.Header.Set("Content-Type", "application/json") @@ -123,12 +51,12 @@ func make_request(url string, method string, data map[string]interface{}) ([]byt client := &http.Client{} resp, err := client.Do(req) if err != nil { - panic(err) + return nil,err } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) - return body + return body, nil } func resp_to_json(body []byte) (map[string]string) { @@ -141,11 +69,14 @@ func resp_to_json(body []byte) (map[string]string) { return respData } -func namespace_register_with_identity(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, prefix string) () { +func namespace_register_with_identity(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, prefix string) (error) { data := map[string]interface{}{ "identity_required": "true", } - resp := make_request(namespaceRegistryEndpoint, "POST", data) + resp, err := make_request(namespaceRegistryEndpoint, "POST", data) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } respData := resp_to_json(resp) verification_url := respData["verification_url"] @@ -158,7 +89,10 @@ func namespace_register_with_identity(publicKeyPath string, privateKeyPath strin "identity_required": "true", "device_code": device_code, } - resp = make_request(namespaceRegistryEndpoint, "POST", data) + resp, err = make_request(namespaceRegistryEndpoint, "POST", data) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } respData = resp_to_json(resp) if respData["status"] == "APPROVED" { @@ -168,40 +102,43 @@ func namespace_register_with_identity(publicKeyPath string, privateKeyPath strin reader := bufio.NewReader(os.Stdin) fmt.Print("Press Enter after verification") _, _ = reader.ReadString('\n') - // time.Sleep(60 * time.Second) } } access_token := respData["access_token"] fmt.Printf("Access token: %s\n", access_token) - namespace_register(publicKeyPath, privateKeyPath, namespaceRegistryEndpoint, access_token, prefix) - + return namespace_register(publicKeyPath, privateKeyPath, namespaceRegistryEndpoint, access_token, prefix) } -func namespace_register(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, access_token string, prefix string) () { - publicKey, err := loadPublicKey(publicKeyPath) +func namespace_register(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, access_token string, prefix string) (error) { + publicKey, err := config.LoadPublicKey(publicKeyPath, privateKeyPath) + if err != nil { + return fmt.Errorf("Failed to load public key: %v\n", err) + } + + jwks, err := config.JWKSMap(publicKey) if err != nil { - fmt.Printf("Failed to load public key: %v\n", err) - os.Exit(1) + return fmt.Errorf("Failed to convert public key to JWKS: %v\n", err) } - privateKey, err := loadPrivateKey(privateKeyPath) + privateKey, err := config.LoadPrivateKey(privateKeyPath) if err != nil { - fmt.Printf("Failed to load private key: %v\n", err) - os.Exit(1) + return fmt.Errorf("Failed to load private key: %v\n", err) } client_nonce, err := generateNonce() if err != nil { - fmt.Printf("Failed to generate nonce: %v\n", err) - os.Exit(1) + return fmt.Errorf("Failed to generate client nonce: %v\n", err) } data := map[string]interface{}{ "client_nonce": client_nonce, - "pubkey": fmt.Sprintf("%x", publicKey.X.Bytes()), + "pubkey": fmt.Sprintf("%x", jwks["x"]), } - resp := make_request(namespaceRegistryEndpoint, "POST", data) + resp, err := make_request(namespaceRegistryEndpoint, "POST", data) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } respData := resp_to_json(resp) // Create client payload by concatenating client_nonce and server_nonce @@ -210,17 +147,20 @@ func namespace_register(publicKeyPath string, privateKeyPath string, namespaceRe // Sign the payload signature, err := signPayload([]byte(clientPayload), privateKey) if err != nil { - panic(err) + return fmt.Errorf("Failed to sign payload: %v\n", err) } // Create data for the second POST request + xBytes, _ := base64.RawURLEncoding.DecodeString(jwks["x"]) + yBytes, _ := base64.RawURLEncoding.DecodeString(jwks["y"]) + data2 := map[string]interface{}{ "client_nonce": client_nonce, "server_nonce": respData["server_nonce"], "pubkey": map[string]string{ - "x": publicKey.X.String(), - "y": publicKey.Y.String(), - "curve": "P-521", + "x" : new(big.Int).SetBytes(xBytes).String(), + "y" : new(big.Int).SetBytes(yBytes).String(), + "curve": jwks["crv"], }, "client_payload": clientPayload, "client_signature": hex.EncodeToString(signature), @@ -231,23 +171,36 @@ func namespace_register(publicKeyPath string, privateKeyPath string, namespaceRe } // Send the second POST request - make_request(namespaceRegistryEndpoint, "POST", data2) - fmt.Printf("Namespace registered successfully\n") + _, err = make_request(namespaceRegistryEndpoint, "POST", data2) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } + return nil } -func list_namespaces(endpoint string) { - respData := make_request(endpoint, "GET", nil) +func list_namespaces(endpoint string) (error) { + respData, err := make_request(endpoint, "GET", nil) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } fmt.Println(string(respData)) + return nil } -func get_namespace(endpoint string) { - respData := make_request(endpoint, "GET", nil) +func get_namespace(endpoint string) (error) { + respData, err := make_request(endpoint, "GET", nil) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } fmt.Println(string(respData)) + return nil } -func delete_namespace(endpoint string) { - respData := make_request(endpoint, "DELETE", nil) +func delete_namespace(endpoint string) (error) { + respData, err := make_request(endpoint, "DELETE", nil) + if err != nil { + return fmt.Errorf("Failed to make request: %v\n", err) + } fmt.Println(string(respData)) -} - - + return nil +} \ No newline at end of file diff --git a/config/init_server_creds.go b/config/init_server_creds.go index 39c0bae40..c627aa70e 100644 --- a/config/init_server_creds.go +++ b/config/init_server_creds.go @@ -13,6 +13,7 @@ import ( "os" "sync/atomic" "time" + "encoding/json" "github.com/lestrrat-go/jwx/jwk" "github.com/pkg/errors" @@ -23,6 +24,72 @@ var ( privateKey atomic.Pointer[jwk.Key] ) +func LoadPrivateKey(tlsKey string)(*ecdsa.PrivateKey, error) { + fmt.Println("new load Private Key") + rest, err := os.ReadFile(tlsKey) + if err != nil { + return nil, nil + } + + var privateKey *ecdsa.PrivateKey + var block *pem.Block + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } else if block.Type == "PRIVATE KEY" { + genericPrivateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + switch key := genericPrivateKey.(type) { + case *ecdsa.PrivateKey: + privateKey = key + default: + return nil, fmt.Errorf("Unsupported private key type: %T", key) + } + break + } + } + if privateKey == nil { + return nil, fmt.Errorf("Private key file, %v, contains no private key", tlsKey) + } + return privateKey, nil +} + +func LoadPublicKey(existingJWKS string, issuerKeyFile string) (*jwk.Set, error) { + jwks := jwk.NewSet() + if existingJWKS != "" { + var err error + jwks, err = jwk.ReadFile(existingJWKS) + if err != nil { + return nil, errors.Wrap(err, "Failed to read issuer JWKS file") + } + } + + if err := GeneratePrivateKey(issuerKeyFile); err != nil { + return nil, err + } + contents, err := os.ReadFile(issuerKeyFile) + if err != nil { + return nil, errors.Wrap(err, "Failed to read issuer key file") + } + key, err := jwk.ParseKey(contents, jwk.WithPEM(true)) + if err != nil { + return nil, errors.Wrapf(err, "Failed to parse issuer key file %v", issuerKeyFile) + } + pkey, err := jwk.PublicKeyOf(key) + if err != nil { + return nil, errors.Wrapf(err, "Failed to generate public key from file %v", issuerKeyFile) + } + err = jwk.AssignKeyID(pkey) + if err != nil { + return nil, err + } + jwks.Add(pkey) + return &jwks, nil +} + func GenerateCert() error { gid, err := GetDaemonGID() if err != nil { @@ -46,34 +113,11 @@ func GenerateCert() error { } tlsKey := viper.GetString("TLSKey") - rest, err := os.ReadFile(tlsKey) + privateKey, err := LoadPrivateKey(tlsKey) if err != nil { - return nil + return err } - var privateKey *ecdsa.PrivateKey - var block *pem.Block - for { - block, rest = pem.Decode(rest) - if block == nil { - break - } else if block.Type == "PRIVATE KEY" { - genericPrivateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return err - } - switch key := genericPrivateKey.(type) { - case *ecdsa.PrivateKey: - privateKey = key - default: - return fmt.Errorf("Unsupported private key type: %T", key) - } - break - } - } - if privateKey == nil { - return fmt.Errorf("Private key file, %v, contains no private key", tlsKey) - } serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { @@ -154,7 +198,6 @@ func GeneratePrivateKey(keyLocation string) error { keyLocation, groupname) } - bytes, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return err @@ -168,37 +211,8 @@ func GeneratePrivateKey(keyLocation string) error { func GenerateIssuerJWKS() (*jwk.Set, error) { existingJWKS := viper.GetString("IssuerJWKS") - jwks := jwk.NewSet() - if existingJWKS != "" { - var err error - jwks, err = jwk.ReadFile(existingJWKS) - if err != nil { - return nil, errors.Wrap(err, "Failed to read issuer JWKS file") - } - } issuerKeyFile := viper.GetString("IssuerKey") - if err := GeneratePrivateKey(issuerKeyFile); err != nil { - return nil, err - } - contents, err := os.ReadFile(issuerKeyFile) - if err != nil { - return nil, errors.Wrap(err, "Failed to read issuer key file") - } - key, err := jwk.ParseKey(contents, jwk.WithPEM(true)) - if err != nil { - return nil, errors.Wrapf(err, "Failed to parse issuer key file %v", issuerKeyFile) - } - pkey, err := jwk.PublicKeyOf(key) - if err != nil { - return nil, errors.Wrapf(err, "Failed to generate public key from file %v", issuerKeyFile) - } - err = jwk.AssignKeyID(pkey) - if err != nil { - return nil, err - } - jwks.Add(pkey) - return &jwks, nil - + return LoadPublicKey(existingJWKS, issuerKeyFile) } func GetOriginJWK() (*jwk.Key, error) { @@ -218,3 +232,30 @@ func GetOriginJWK() (*jwk.Key, error) { } return key, nil } + +func JWKSMap(jwks *jwk.Set) (map[string]string, error) { + // Marshal the set into JSON + jsonBytes, err := json.MarshalIndent(jwks, "", " ") + if err != nil { + return nil, err + } + + // Parse the JSON into a structure we can manipulate + var parsed map[string][]map[string]interface{} + err = json.Unmarshal(jsonBytes, &parsed) + if err != nil { + return nil, err + } + + // Convert the map[string]interface{} to map[string]string + stringMaps := make([]map[string]string, len(parsed["keys"])) + for i, m := range parsed["keys"] { + stringMap := make(map[string]string) + for k, v := range m { + stringMap[k] = fmt.Sprintf("%v", v) + } + stringMaps[i] = stringMap + } + + return stringMaps[0], nil +} \ No newline at end of file From 7193d648f3e76c2ee27798f0b6c33f70b6b01c4b Mon Sep 17 00:00:00 2001 From: Yuxiao Qu Date: Mon, 31 Jul 2023 09:16:44 -0500 Subject: [PATCH 7/8] resolve conflict with main branch, the GeneratePrivateKey() in init_server_creds.go now also takes a curve argument as input --- config/init_server_creds.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/init_server_creds.go b/config/init_server_creds.go index 6b59abb97..1850588b2 100644 --- a/config/init_server_creds.go +++ b/config/init_server_creds.go @@ -67,7 +67,7 @@ func LoadPublicKey(existingJWKS string, issuerKeyFile string) (*jwk.Set, error) } } - if err := GeneratePrivateKey(issuerKeyFile); err != nil { + if err := GeneratePrivateKey(issuerKeyFile, elliptic.P521()); err != nil { return nil, err } contents, err := os.ReadFile(issuerKeyFile) From eb0c2eb18e71b3470e695aa0341d3f4fa0fee76d Mon Sep 17 00:00:00 2001 From: Brian P Bockelman Date: Tue, 1 Aug 2023 07:48:19 -0500 Subject: [PATCH 8/8] Remove unnecessary debug output --- config/init_server_creds.go | 1 - 1 file changed, 1 deletion(-) diff --git a/config/init_server_creds.go b/config/init_server_creds.go index 1850588b2..923d820d3 100644 --- a/config/init_server_creds.go +++ b/config/init_server_creds.go @@ -25,7 +25,6 @@ var ( ) func LoadPrivateKey(tlsKey string)(*ecdsa.PrivateKey, error) { - fmt.Println("new load Private Key") rest, err := os.ReadFile(tlsKey) if err != nil { return nil, nil