Skip to content

Commit

Permalink
Merge pull request PelicanPlatform#1090 from haoming29/cli-keygen
Browse files Browse the repository at this point in the history
Generate public/private keys and server web ui htpasswd files
  • Loading branch information
haoming29 authored May 2, 2024
2 parents d53b5f8 + 334f902 commit a54291a
Show file tree
Hide file tree
Showing 7 changed files with 545 additions and 0 deletions.
71 changes: 71 additions & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/***************************************************************
*
* Copyright (C) 2024, Pelican Project, Morgridge Institute for Research
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************/

package main

import (
"github.com/spf13/cobra"
)

var (
generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate credentials for Pelican server",
Long: "",
}

passwordCmd = &cobra.Command{
Use: "password",
Short: "Generate a Pelican admin website password file (htpasswd)",
Long: `Given a password for the admin website, generate the htpasswd file that Pelican server
uses to store the password and authenticate the admin user. You may put the generated file under
/etc/pelican with name "server-web-passwd", or change Server.UIPasswordFile
to the path to generated file to initialize the admin website.
`,
RunE: passwordMain,
SilenceUsage: true,
}

keygenCmd = &cobra.Command{
Use: "keygen",
Short: "Generate a public-private key-pair for Pelican OIDC issuer",
Long: `Generate a public-private key-pair for a Pelican server.
The private key is a ECDSA key with P256 curve. The corresponding public key
is a JWKS in JSON. The public key follows OIDC protocol and can be used
for JWT signature verification.
`,
RunE: keygenMain,
SilenceUsage: true,
}

outPasswordPath string
inPasswordPath string

privateKeyPath string
publicKeyPath string
)

func init() {
generateCmd.AddCommand(keygenCmd, passwordCmd)

passwordCmd.Flags().StringVarP(&outPasswordPath, "output", "o", "", "The path to the generate htpasswd password file. Default: ./server-web-passwd")
passwordCmd.Flags().StringVarP(&inPasswordPath, "password", "p", "", "The path to the file containing the password. Will take from terminal input if not provided")

keygenCmd.Flags().StringVar(&privateKeyPath, "private-key", "", "The path to the generate private key file. Default: ./issuer.jwk")
keygenCmd.Flags().StringVar(&publicKeyPath, "public-key", "", "The path to the generate public key file. Default: ./issuer-pub.jwks")
}
87 changes: 87 additions & 0 deletions cmd/generate_keygen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/***************************************************************
*
* Copyright (C) 2024, Pelican Project, Morgridge Institute for Research
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************/

package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/pelicanplatform/pelican/config"
"github.com/pelicanplatform/pelican/param"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func keygenMain(cmd *cobra.Command, args []string) error {
wd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "failed to get the current working directory")
}
if privateKeyPath == "" {
privateKeyPath = filepath.Join(wd, "issuer.jwk")
} else {
privateKeyPath = filepath.Clean(strings.TrimSpace(privateKeyPath))
}

if err = os.MkdirAll(filepath.Dir(privateKeyPath), 0755); err != nil {
return errors.Wrapf(err, "failed to create directory for private key at %s", filepath.Dir(privateKeyPath))
}

if publicKeyPath == "" {
publicKeyPath = filepath.Join(wd, "issuer-pub.jwks")
} else {
publicKeyPath = filepath.Clean(strings.TrimSpace(publicKeyPath))
}

_, err = os.Stat(privateKeyPath)
if err == nil {
return fmt.Errorf("file exists for private key under %s", privateKeyPath)
}

_, err = os.Stat(publicKeyPath)
if err == nil {
return fmt.Errorf("file exists for public key under %s", publicKeyPath)
}

if err = os.MkdirAll(filepath.Dir(publicKeyPath), 0755); err != nil {
return errors.Wrapf(err, "failed to create directory for public key at %s", filepath.Dir(privateKeyPath))
}

viper.Set(param.IssuerKey.GetName(), privateKeyPath)

// GetIssuerPublicJWKS will generate the private key at IssuerKey if it does not exist
// and parse the private key and generate the corresponding public key for us
pubkey, err := config.GetIssuerPublicJWKS()
if err != nil {
return err
}
bytes, err := json.MarshalIndent(pubkey, "", " ")
if err != nil {
return errors.Wrap(err, "failed to generate json from jwks")
}
if err = os.WriteFile(publicKeyPath, bytes, 0644); err != nil {
return errors.Wrap(err, "fail to write the public key to the file")
}
fmt.Printf("Successfully generated keys at: \nPrivate key: %s\nPublic Key: %s\n", privateKeyPath, publicKeyPath)
return nil
}
162 changes: 162 additions & 0 deletions cmd/generate_keygen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/***************************************************************
*
* Copyright (C) 2024, Pelican Project, Morgridge Institute for Research
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************/

package main

import (
"os"
"path/filepath"
"testing"

"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pelicanplatform/pelican/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func setupTempWd(t *testing.T) string {
tmpDir := t.TempDir()
wd, err := os.Getwd()
require.NoError(t, err)
err = os.Chdir(tmpDir)
require.NoError(t, err)
t.Cleanup(func() {
err := os.Chdir(wd)
require.NoError(t, err)
})
return tmpDir
}

func checkKeys(t *testing.T, privateKey, publicKey string) {
_, err := config.LoadPrivateKey(privateKey, false)
require.NoError(t, err)

jwks, err := jwk.ReadFile(publicKey)
require.NoError(t, err)
require.Equal(t, 1, jwks.Len())
key, ok := jwks.Key(0)
assert.True(t, ok)
err = key.Validate()
assert.NoError(t, err)
}

func TestKeygenMain(t *testing.T) {
t.Run("no-args-gen-to-wd", func(t *testing.T) {
tempDir := setupTempWd(t)

privateKeyPath = ""
publicKeyPath = ""
err := keygenMain(nil, []string{})
require.NoError(t, err)

checkKeys(
t,
filepath.Join(tempDir, "issuer.jwk"),
filepath.Join(tempDir, "issuer-pub.jwks"),
)
})

t.Run("private-arg-present", func(t *testing.T) {
tempDir := t.TempDir()
tempWd := setupTempWd(t)

privateKeyPath = filepath.Join(tempDir, "test.pk")
publicKeyPath = ""
err := keygenMain(nil, []string{})
require.NoError(t, err)

checkKeys(
t,
privateKeyPath,
filepath.Join(tempWd, "issuer-pub.jwks"),
)
})

t.Run("public-arg-present", func(t *testing.T) {
tempDir := t.TempDir()
tempWd := setupTempWd(t)

privateKeyPath = ""
publicKeyPath = filepath.Join(tempDir, "test.pub")
err := keygenMain(nil, []string{})
require.NoError(t, err)

checkKeys(
t,
filepath.Join(tempWd, "issuer.jwk"),
publicKeyPath,
)
})

t.Run("private-arg-with-newline", func(t *testing.T) {
tempDir := t.TempDir()
tempWd := setupTempWd(t)

privateKeyPath = filepath.Join(tempDir, "test.pk")
privateKeyPath += "\n"
publicKeyPath = ""
err := keygenMain(nil, []string{})
require.NoError(t, err)

checkKeys(
t,
privateKeyPath,
filepath.Join(tempWd, "issuer-pub.jwks"),
)
})

t.Run("public-arg-with-newline", func(t *testing.T) {
tempDir := t.TempDir()
tempWd := setupTempWd(t)

privateKeyPath = ""
publicKeyPath = filepath.Join(tempDir, "test.pub")
publicKeyPath += "\n"
err := keygenMain(nil, []string{})
require.NoError(t, err)

checkKeys(
t,
filepath.Join(tempWd, "issuer.jwk"),
publicKeyPath,
)
})

t.Run("private-key-exists", func(t *testing.T) {
tempDir := t.TempDir()

err := os.WriteFile(filepath.Join(tempDir, "test.pk"), []byte{}, 0644)
require.NoError(t, err)
privateKeyPath = filepath.Join(tempDir, "test.pk")
publicKeyPath = filepath.Join(tempDir, "test.pub")
err = keygenMain(nil, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), "file exists")
})

t.Run("public-key-exists", func(t *testing.T) {
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "test.pub"), []byte{}, 0644)
require.NoError(t, err)
privateKeyPath = filepath.Join(tempDir, "test.pk")
publicKeyPath = filepath.Join(tempDir, "test.pub")
err = keygenMain(nil, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), "file exists")
})
}
Loading

0 comments on commit a54291a

Please sign in to comment.