diff --git a/cmd/namespace.go b/cmd/namespace.go new file mode 100644 index 000000000..c2d74b76b --- /dev/null +++ b/cmd/namespace.go @@ -0,0 +1,116 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" + log "github.com/sirupsen/logrus" +) + +// 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 == "" { + log.Error("Error: prefix is required") + os.Exit(1) + } + + if withIdentity { + err := namespace_register_with_identity(pubkeyPath, privkeyPath, endpoint, prefix) + if err != nil { + log.Error(err) + os.Exit(1) + } + } else { + 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 + err := delete_namespace(endpoint) + if err != nil { + log.Error(err) + os.Exit(1) + } +} + +func listAllNamespaces(cmd *cobra.Command, args []string) { + endpoint := host + 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" + err := get_namespace(endpoint) + if err != nil { + log.Error(err) + os.Exit(1) + } + } else { + log.Error("Error: get command requires --jwks flag") + os.Exit(1) + } +} + +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..703af373b --- /dev/null +++ b/cmd/namespace_registry.go @@ -0,0 +1,206 @@ +package main + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "net/http" + "bytes" + "bufio" + "math/big" + "encoding/base64" + + "github.com/pelicanplatform/pelican/config" +) + + +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 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, error) { + payload, _ := json.Marshal(data) + + req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil,err + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + return body, nil +} + +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) (error) { + data := map[string]interface{}{ + "identity_required": "true", + } + 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"] + 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, 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" { + done = true + } else { + fmt.Printf("Waiting for approval...\n") + reader := bufio.NewReader(os.Stdin) + fmt.Print("Press Enter after verification") + _, _ = reader.ReadString('\n') + } + } + access_token := respData["access_token"] + fmt.Printf("Access token: %s\n", access_token) + return namespace_register(publicKeyPath, privateKeyPath, namespaceRegistryEndpoint, access_token, prefix) +} + +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 { + return fmt.Errorf("Failed to convert public key to JWKS: %v\n", err) + } + + privateKey, err := config.LoadPrivateKey(privateKeyPath) + if err != nil { + return fmt.Errorf("Failed to load private key: %v\n", err) + } + + client_nonce, err := generateNonce() + if err != nil { + return fmt.Errorf("Failed to generate client nonce: %v\n", err) + } + + data := map[string]interface{}{ + "client_nonce": client_nonce, + "pubkey": fmt.Sprintf("%x", jwks["x"]), + } + + 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 + clientPayload := client_nonce + respData["server_nonce"] + + // Sign the payload + signature, err := signPayload([]byte(clientPayload), privateKey) + if err != nil { + 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" : new(big.Int).SetBytes(xBytes).String(), + "y" : new(big.Int).SetBytes(yBytes).String(), + "curve": jwks["crv"], + }, + "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 + _, 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) (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) (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) (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/cmd/origin.go b/cmd/origin.go index 2d6412f46..7b36a5e0b 100644 --- a/cmd/origin.go +++ b/cmd/origin.go @@ -37,8 +37,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 e4819dc41..11dfd1484 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,6 +42,7 @@ func init() { rootCmd.AddCommand(directorCmd) objectCmd.CompletionOptions.DisableDefaultCmd = true rootCmd.AddCommand(originCmd) + rootCmd.AddCommand(namespaceCmd) rootCmd.AddCommand(rootConfigCmd) rootCmd.AddCommand(rootPluginCmd) preferredPrefix := config.GetPreferredPrefix() @@ -88,4 +89,4 @@ func initConfig() { if viper.GetBool("Debug") { setLogging(log.DebugLevel) } -} +} \ No newline at end of file diff --git a/config/init_server_creds.go b/config/init_server_creds.go index c47af99a1..923d820d3 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,71 @@ var ( privateKey atomic.Pointer[jwk.Key] ) +func LoadPrivateKey(tlsKey string)(*ecdsa.PrivateKey, error) { + 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, elliptic.P521()); 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 +112,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 +197,6 @@ func GeneratePrivateKey(keyLocation string, curve elliptic.Curve) error { keyLocation, groupname) } - bytes, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return err @@ -168,37 +210,8 @@ func GeneratePrivateKey(keyLocation string, curve elliptic.Curve) 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, elliptic.P521()); 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 +231,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