Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving --signer-fulcio-token flag to accept both path and raw token string #82

Merged
merged 16 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hack/test.token
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ.3Thp81rDFrKXr3WrY1MyMnNK8kKoZBX9lg-JwFznR-M
22 changes: 20 additions & 2 deletions signer/fulcio/fulcio.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func init() {
),
registry.StringConfigOption(
"token",
"Raw token to use for authentication",
"Raw token to use for authentication. The token or a path to the file containing the token is accepted",
"",
func(sp signer.SignerProvider, token string) (signer.SignerProvider, error) {
fsp, ok := sp.(FulcioSignerProvider)
Expand Down Expand Up @@ -211,7 +211,11 @@ func (fsp FulcioSignerProvider) Signer(ctx context.Context) (cryptoutil.Signer,
}

case fsp.Token != "":
raw = fsp.Token
// reading from file if a path was supplied
raw, err = idToken(fsp.Token)
if err != nil {
return nil, err
}

case fsp.Token == "" && isatty.IsTerminal(os.Stdin.Fd()):
tok, err := oauthflow.OIDConnect(fsp.OidcIssuer, fsp.OidcClientID, "", "", oauthflow.DefaultIDTokenGetter)
Expand Down Expand Up @@ -403,3 +407,17 @@ func newClient(ctx context.Context, fulcioURL string, fulcioPort int, isInsecure
// Create the Fulcio client
return fulciopb.NewCAClient(conn), nil
}

// idToken tries to parse a string as a token in JWS form. If it fails,
// it treats the string as a path and tries to open the file at that path
func idToken(s string) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://dave.cheney.net/2019/07/09/clear-is-better-than-clever
https://go-proverbs.github.io/


The design of a function to fall back from one behavior to another can be both practical and user-friendly, but it also has potential drawbacks that need to be considered. Here are some points to consider regarding the design of the idToken function:

Pros:

  1. User Convenience: The function provides flexibility by accepting either a raw token or a file path. This can be convenient for users with different forms of tokens.

  2. Simplicity: The function abstracts away the details of how the token is obtained, simplifying the interface for the caller.

  3. Fallback Mechanism: A fallback mechanism can be a good defensive programming practice, ensuring the function has an alternative way to proceed if the first method fails.

Cons:

  1. Ambiguity: The dual-purpose nature of the function's argument can lead to ambiguity. It's not immediately clear from the function signature what the expected input is.

  2. Error Handling: If the input is neither a valid token nor a valid file path, the error returned may not be informative enough to help the user understand the issue.

  3. Security Concerns: Automatically treating the input as a file path if it's not a valid token could lead to security issues, such as unintentional file reads.

  4. Single Responsibility Principle: The function parses a token and reads a file. This goes against the Single Responsibility Principle, which suggests that a function should do one thing only.

  5. Testing Complexity: The function may be harder to test because it has multiple execution paths depending on the input.


Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@naveensrinivasan - thank you so much for this comment! I was thinking exactly this when I was creating the PR. @mikhailswift @jkjell @kairoaraujo @colek42 and others, it would be good to get your perspectives on this one so we can decide a way to proceed with the PR 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s a good comment.

I’d definitely recommend splitting up the functionality. Also from a security perspective would be more defensive to treat the suspected JWS as only ever being a JWS even if it fails to parse and have a different path for matching to a file.

Perhaps a different flag or envvar or however else it’s getting picked up.

Being explicit is better than implicit in this case I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @fkautz . There's a certain nicety when things just happen for you implicitly, which is always attractive to make things convenient, but I tend to lean toward explicit options being better for use cases like this, especially due to the logic of "well this isn't a token, so let's try opening it like a file" being particularly scary.

// If this is a valid raw token, just return it
// NOTE: could be replaced with https://pkg.go.dev/go.step.sm/crypto/jose in future if features helpful
if _, err := jwt.ParseSigned(s); err == nil {
return s, nil
}

// Otherwise, if this is a path to a token return the contents
c, err := os.ReadFile(s)
return string(c), err
}
33 changes: 33 additions & 0 deletions signer/fulcio/fulcio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"fmt"
"log"
"net"
"os"
"strings"
"testing"
"time"
Expand All @@ -33,6 +34,7 @@
fulciopb "github.com/sigstore/fulcio/pkg/generated/protobuf"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"path/filepath"

"google.golang.org/grpc"
"gopkg.in/square/go-jose.v2/jwt"
Expand Down Expand Up @@ -76,6 +78,37 @@
require.NotNil(t, client)
}

func TestIDToken(t *testing.T) {
// test when supplying a raw token, the same token is returned
tok := generateTestToken("[email protected]", "testsubject")
out, err := idToken(tok)
require.NoError(t, err)
require.Equal(t, tok, out)

// test when supplying a path, a valid token is returned
// NOTE: this function could be refactored to accept a fileSystem or io.Reader so reading the file can be mocked,
// but unsure if this is the way we want to go for now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea. The less code we have in the test (i.e. composing a file path) the better. I think you could also do a relative path: ../../hack/test.token. I also don't have a strong preference on where to put test data like that.

wd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
rootDir := filepath.Dir(filepath.Dir(wd))
tok = filepath.Join(rootDir, "hack", "test.token")
testTok, err := os.ReadFile(tok)
if err != nil {
t.Fatalf("failed to read test token file: %v", err)
}

out, err = idToken(tok)
require.NoError(t, err)
require.Equal(t, string(testTok), out)

// test that when neither valid token nor a path is supplied, an error is returned
tok = "test"
out, err = idToken(tok)

Check failure on line 108 in signer/fulcio/fulcio_test.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to out (ineffassign)
require.Error(t, err)
}

type dummyCAClientService struct {
client fulciopb.CAClient
server *grpc.Server
Expand Down
Loading