diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8c1e1b19 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.go] +indent_style = tab + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{json,md}] +indent_style = space +indent_size = 2 + +[*.sh] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/commands/login.go b/commands/login.go index bdd15f1e..fb5987cd 100644 --- a/commands/login.go +++ b/commands/login.go @@ -93,7 +93,8 @@ type LoginCmd struct { ProviderArg string // OpenID Provider specification in the format: , or ,, or ,,, ProviderAliasArg string KeyTypeArg KeyType - PrintKeyArg bool // Print private key and SSH cert instead of writing them to the filesystem + PrintKeyArg bool // Print the raw private key and SSH cert to stdout instead of writing them to the filesystem + InspectCertArg bool // Display a human-readable inspection of the generated SSH certificate (public information only) SSHConfigured bool Verbosity int // Default verbosity is 0, 1 is verbose, 2 is debug RemoteRedirectURI string @@ -117,7 +118,7 @@ type LoginCmd struct { func NewLogin(autoRefreshArg bool, configPathArg string, createConfigArg bool, configureArg bool, logDirArg string, sendAccessTokenArg bool, disableBrowserOpenArg bool, printIdTokenArg bool, providerArg string, printKeyArg bool, keyPathArg string, providerAliasArg string, keyTypeArg KeyType, - remoteRedirectUri string, + remoteRedirectUri string, inspectCertArg bool, ) *LoginCmd { return &LoginCmd{ Fs: afero.NewOsFs(), @@ -132,6 +133,7 @@ func NewLogin(autoRefreshArg bool, configPathArg string, createConfigArg bool, c KeyPathArg: keyPathArg, ProviderArg: providerArg, PrintKeyArg: printKeyArg, + InspectCertArg: inspectCertArg, ProviderAliasArg: providerAliasArg, KeyTypeArg: keyTypeArg, RemoteRedirectURI: remoteRedirectUri, @@ -511,7 +513,14 @@ func (l *LoginCmd) login(ctx context.Context, provider providers.OpenIdProvider, return nil, fmt.Errorf("failed to format ID Token: %w", err) } - fmt.Printf("id_token:\n%s\n", idTokenStr) + fmt.Fprintf(l.out(), "id_token:\n%s\n", idTokenStr) + } + + if l.InspectCertArg { + inspect := NewInspectCmd(string(certBytes), l.out()) + if err := inspect.Run(); err != nil { + return nil, fmt.Errorf("failed to inspect SSH cert: %w", err) + } } idStr, err := IdentityString(*pkt) @@ -865,7 +874,7 @@ func PrettyIdToken(pkt pktoken.PKToken) (string, error) { if err != nil { return "", err } - idtJson, err := json.MarshalIndent(idt.GetClaims(), "", " ") + idtJson, err := json.MarshalIndent(idt.GetClaims(), "", " ") if err != nil { return "", err } diff --git a/commands/login_test.go b/commands/login_test.go index fea7699f..b99b6a47 100644 --- a/commands/login_test.go +++ b/commands/login_test.go @@ -155,6 +155,16 @@ func TestLoginCmd(t *testing.T) { }, wantError: false, }, + { + name: "Good path InspectCert", + envVars: map[string]string{}, + loginCmd: LoginCmd{ + Verbosity: 0, + InspectCertArg: true, + LogDirArg: logDir, + }, + wantError: false, + }, { name: "Good path with SendAccessToken set in arg and config", envVars: map[string]string{}, @@ -233,6 +243,17 @@ func TestLoginCmd(t *testing.T) { require.Contains(t, gotLines[0], "cert-v01@openssh.com AAAA") require.Contains(t, gotLines[1], "-----BEGIN OPENSSH PRIVATE KEY-----") pubKeyBytes = []byte(gotLines[0]) + } else if tt.loginCmd.InspectCertArg { + got := cliOutputBuffer.String() + require.Contains(t, got, "--- SSH Certificate Information ---") + require.Contains(t, got, "--- PKToken Structure ---") + + homePath, err := os.UserHomeDir() + require.NoError(t, err) + // KeyTypeArg defaults to ECDSA so keys are written to id_ecdsa path + sshPubPath := filepath.Join(homePath, ".ssh", "id_ecdsa-cert.pub") + pubKeyBytes, err = afero.ReadFile(mockFs, sshPubPath) + require.NoError(t, err) } else { homePath, err := os.UserHomeDir() require.NoError(t, err) @@ -447,7 +468,7 @@ func TestNewLogin(t *testing.T) { remoteRedirectURIArg := "" loginCmd := NewLogin(autoRefresh, configPathArg, createConfig, configureArg, logDir, - sendAccessTokenArg, disableBrowserOpenArg, printIdTokenArg, providerArg, keyAsOutputArg, keyPathArg, providerAlias, keyTypeArg, remoteRedirectURIArg) + sendAccessTokenArg, disableBrowserOpenArg, printIdTokenArg, providerArg, keyAsOutputArg, keyPathArg, providerAlias, keyTypeArg, remoteRedirectURIArg, false) require.NotNil(t, loginCmd) } diff --git a/main.go b/main.go index a07b15f0..af1bfca9 100644 --- a/main.go +++ b/main.go @@ -154,6 +154,8 @@ Arguments: var disableBrowserOpenArg bool var printIdTokenArg bool var printKeyArg bool + var inspectCertArg bool + var verboseArg bool var keyPathArg string var keyTypeArg commands.KeyType var remoteRedirectURIArg string @@ -188,9 +190,13 @@ Arguments: providerAliasArg = args[0] } + if verboseArg { + inspectCertArg = true + } + login := commands.NewLogin(autoRefreshArg, configPathArg, createConfigArg, configureArg, logDirArg, sendAccessTokenArg, disableBrowserOpenArg, printIdTokenArg, providerArg, printKeyArg, keyPathArg, - providerAliasArg, keyTypeArg, remoteRedirectURIArg) + providerAliasArg, keyTypeArg, remoteRedirectURIArg, inspectCertArg) if err := login.Run(ctx); err != nil { log.Println("Error executing login command:", err) return err @@ -210,7 +216,9 @@ Arguments: loginCmd.Flags().BoolVar(&printIdTokenArg, "print-id-token", false, "Set this flag to print out the contents of the id_token. Useful for inspecting claims") loginCmd.Flags().BoolVar(&sendAccessTokenArg, "send-access-token", false, "Set this flag to send the Access Token as well as the PK Token in the SSH cert. The Access Token is used to call the userinfo endpoint to get claims not included in the ID Token") loginCmd.Flags().StringVar(&providerArg, "provider", "", "OpenID Provider specification in the format: , or ,, or ,,,") - loginCmd.Flags().BoolVarP(&printKeyArg, "print-key", "p", false, "Print private key and SSH cert instead of writing them to the filesystem") + loginCmd.Flags().BoolVarP(&printKeyArg, "print-key", "p", false, "Print the raw private key and SSH cert to stdout instead of writing them to the filesystem") + loginCmd.Flags().BoolVar(&inspectCertArg, "inspect-cert", false, "Print a human-readable inspection of the generated SSH certificate (public information only)") + loginCmd.Flags().BoolVarP(&verboseArg, "verbose", "v", false, "Enable verbose output") loginCmd.Flags().StringVarP(&keyPathArg, "private-key-file", "i", "", "Path where private keys is written") loginCmd.Flags().StringVar(&remoteRedirectURIArg, "remote-redirect-uri", "", "Remote redirect URI used for non-localhost redirects. This is an advanced option for embedding opkssh in server-side logic.") loginCmd.Flags().VarP(enumflag.New(&keyTypeArg, "Key Type", map[commands.KeyType][]string{commands.ECDSA: {commands.ECDSA.String()}, commands.ED25519: {commands.ED25519.String()}}, enumflag.EnumCaseInsensitive), "key-type", "t", "Type of key to generate") diff --git a/main_test.go b/main_test.go index 5c7f89a1..eb49ab8c 100644 --- a/main_test.go +++ b/main_test.go @@ -280,6 +280,18 @@ func TestRun(t *testing.T) { wantOutput: "error getting provider config for alias badalias", wantExit: 1, }, + { + name: "Login Help flag shows inspect-cert", + args: []string{"opkssh", "login", "--help"}, + wantOutput: "--inspect-cert", + wantExit: 0, + }, + { + name: "Login Help flag shows verbose", + args: []string{"opkssh", "login", "--help"}, + wantOutput: "-v, --verbose", + wantExit: 0, + }, { name: "Verify command fail on bad log file path", args: []string{"opkssh", "verify", "arg1", "arg2", "arg3"},