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

Add generating signed JWT tokens #119

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
134 changes: 134 additions & 0 deletions command_jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package main

import (
"crypto"
"crypto/rand"
"crypto/x509"
"fmt"
"strings"
"time"

"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
)

func commandJwt() error {
userIdent, err := findUserIdentity()
if err != nil {
return errors.Wrap(err, "failed to get identity matching specified user-id")
}
if userIdent == nil {
return fmt.Errorf("could not find identity matching specified user-id: %s", *localUserOpt)
}

cert, err := userIdent.Certificate()
if err != nil {
return errors.Wrap(err, "failed to get identity certificate")
}

signer, err := userIdent.Signer()
if err != nil {
return errors.Wrap(err, "failed to get identity signer")
}

method, hash, err := getX509JwtSigningMethod(cert)
if err != nil {
return errors.Wrap(err, "failed to get JWT signing method")
}

id, err := uuid.NewV7()
if err != nil {
return errors.New("failed to create new UUID for JWT")
}

issuedAt := time.Now()
expireSeconds := 600

token := jwt.NewWithClaims(method, jwt.RegisteredClaims{
Issuer: cert.Issuer.CommonName,
Subject: cert.Subject.CommonName,
Audience: []string{},
ExpiresAt: &jwt.NumericDate{Time: issuedAt.Add(time.Second * time.Duration(expireSeconds))},
NotBefore: &jwt.NumericDate{Time: issuedAt},
IssuedAt: &jwt.NumericDate{Time: issuedAt},
ID: id.String(),
})

signingString, err := token.SigningString()
if err != nil {
return errors.New("failed to generate JWT signing string")
}

if !hash.Available() {
return fmt.Errorf("hash function not available: %s", hash)
}

hasher := hash.New()

_, err = hasher.Write([]byte(signingString))
if err != nil {
return errors.Wrap(err, "failed to create digest")
}

sigBytes, err := signer.Sign(rand.Reader, hasher.Sum(nil), hash)
if err != nil {
return errors.Wrap(err, "failed to sign JWT")
}

signedJwt := strings.Join([]string{signingString, jwt.EncodeSegment(sigBytes)}, ".")

_, err = stdout.Write([]byte(signedJwt))
if err != nil {
return errors.Wrap(err, "failed to write out JWT")
}

return nil
}

// convert x509.SignatureAlgorithm to jwt.SigningMethod
func getX509JwtSigningMethod(cert *x509.Certificate) (method jwt.SigningMethod, hash crypto.Hash, err error) {
switch cert.SignatureAlgorithm {
case x509.SHA256WithRSA:
method = jwt.SigningMethodRS256
hash = jwt.SigningMethodRS256.Hash
case x509.SHA384WithRSA:
method = jwt.SigningMethodRS384
hash = jwt.SigningMethodRS384.Hash
case x509.SHA512WithRSA:
method = jwt.SigningMethodRS512
hash = jwt.SigningMethodRS512.Hash
case x509.SHA256WithRSAPSS:
method = jwt.SigningMethodPS256
hash = jwt.SigningMethodPS256.Hash
case x509.SHA384WithRSAPSS:
method = jwt.SigningMethodPS384
hash = jwt.SigningMethodPS384.Hash
case x509.SHA512WithRSAPSS:
method = jwt.SigningMethodPS512
hash = jwt.SigningMethodPS512.Hash
case x509.ECDSAWithSHA256:
method = jwt.SigningMethodES256
hash = jwt.SigningMethodES256.Hash
case x509.ECDSAWithSHA384:
method = jwt.SigningMethodES384
hash = jwt.SigningMethodES384.Hash
case x509.ECDSAWithSHA512:
method = jwt.SigningMethodES512
hash = jwt.SigningMethodES512.Hash
case x509.PureEd25519:
// Ed25519 does not implement crypto.Hash, so it is more difficult to implement
// method = jwt.SigningMethodEdDSA
err = errors.New("the Ed25519 algorithm is not currently supported by smimesign")
case x509.DSAWithSHA1, x509.DSAWithSHA256:
err = errors.New("the DSA algorithm is not supported by JWT")
case x509.SHA1WithRSA, x509.ECDSAWithSHA1:
err = errors.New("the SHA1 hashing algorithm is not supported by JWT")
case x509.MD2WithRSA, x509.MD5WithRSA:
err = errors.New("the MD hashing algorithm family is not supported by JWT")
default:
err = errors.New("could not parse the x509 signature algorithm")
}

return
}
1 change: 1 addition & 0 deletions command_jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
8 changes: 4 additions & 4 deletions command_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ func commandSign() error {
}

// Git is looking for "\n[GNUPG:] SIG_CREATED ", meaning we need to print a
// line before SIG_CREATED. BEGIN_SIGNING seems appropraite. GPG emits this,
// line before SIG_CREATED. BEGIN_SIGNING seems appropriate. GPG emits this,
// though GPGSM does not.
sBeginSigning.emit()

cert, err := userIdent.Certificate()
if err != nil {
return errors.Wrap(err, "failed to get idenity certificate")
return errors.Wrap(err, "failed to get identity certificate")
}

signer, err := userIdent.Signer()
if err != nil {
return errors.Wrap(err, "failed to get idenity signer")
return errors.Wrap(err, "failed to get identity signer")
}

var f io.ReadCloser
Expand Down Expand Up @@ -72,7 +72,7 @@ func commandSign() error {

chain, err := userIdent.CertificateChain()
if err != nil {
return errors.Wrap(err, "failed to get idenity certificate chain")
return errors.Wrap(err, "failed to get identity certificate chain")
}
if chain, err = certsForSignature(chain); err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.12
require (
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gofrs/uuid v4.3.1+incompatible
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.3.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEex
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b h1:K1wa7ads2Bu1PavI6LfBRMYSy6Zi+Rky0OhWBfrmkmY=
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
Expand Down
22 changes: 18 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ var (
// go build -ldflags "-X main.defaultTSA=${https://whatever}"
defaultTSA = ""

// common usage error response if the user provides incompatible options
usageError = errors.New("specify --help, --sign, --verify, or --list-keys")

// Action flags
helpFlag = getopt.BoolLong("help", 'h', "print this help message")
versionFlag = getopt.BoolLong("version", 'v', "print the version number")
signFlag = getopt.BoolLong("sign", 's', "make a signature")
jwtFlag = getopt.BoolLong("jwt", 'j', "create and sign a JSON Web Token (JWT)")
verifyFlag = getopt.BoolLong("verify", 0, "verify a signature")
listKeysFlag = getopt.BoolLong("list-keys", 0, "show keys")

Expand Down Expand Up @@ -90,17 +94,27 @@ func runCommand() error {

if *signFlag {
if *verifyFlag || *listKeysFlag {
return errors.New("specify --help, --sign, --verify, or --list-keys")
return usageError
} else if len(*localUserOpt) == 0 {
return errors.New("specify a USER-ID to sign with")
} else {
return commandSign()
}
}

if *jwtFlag {
if *signFlag || *listKeysFlag {
return usageError
} else if len(*localUserOpt) == 0 {
return errors.New("specify a USER-ID to sign with")
} else {
return commandJwt()
}
}

if *verifyFlag {
if *signFlag || *listKeysFlag {
return errors.New("specify --help, --sign, --verify, or --list-keys")
return usageError
} else if len(*localUserOpt) > 0 {
return errors.New("local-user cannot be specified for verification")
} else if *detachSignFlag {
Expand All @@ -114,7 +128,7 @@ func runCommand() error {

if *listKeysFlag {
if *signFlag || *verifyFlag {
return errors.New("specify --help, --sign, --verify, or --list-keys")
return usageError
} else if len(*localUserOpt) > 0 {
return errors.New("local-user cannot be specified for list-keys")
} else if *detachSignFlag {
Expand All @@ -126,5 +140,5 @@ func runCommand() error {
}
}

return errors.New("specify --help, --sign, --verify, or --list-keys")
return usageError
}
15 changes: 8 additions & 7 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,16 @@ func resetIO() {

// setup for a test
//
// - parses provided args
// - sets the failer to be a function that fails the test. returns a reset
// function that should be deferred.
// - parses provided args
// - sets the failer to be a function that fails the test. returns a reset
// function that should be deferred.
//
// Example:
// func TestFoo(t *testing.T) {
// defer testSetup(t, "--sign")()
// ...
// }
//
// func TestFoo(t *testing.T) {
// defer testSetup(t, "--sign")()
// ...
// }
func testSetup(t *testing.T, args ...string) func() {
t.Helper()

Expand Down