diff --git a/make/photon/common/install_cert.sh b/make/photon/common/install_cert.sh index a5472b0fd..2e2566940 100755 --- a/make/photon/common/install_cert.sh +++ b/make/photon/common/install_cert.sh @@ -27,7 +27,7 @@ if [[ -d /harbor_cust_cert && -n "$(ls -A /harbor_cust_cert)" ]]; then case ${z} in *.crt | *.ca | *.ca-bundle | *.pem) if [ -d "$z" ]; then - echo "$z is dirictory, skip it ..." + echo "$z is directory, skip it ..." else cat $z >> /etc/pki/tls/certs/ca-bundle.crt echo " $z Appended ..." diff --git a/src/core/service/token/authutils.go b/src/core/service/token/authutils.go index 58e978754..e5ba6e8a1 100644 --- a/src/core/service/token/authutils.go +++ b/src/core/service/token/authutils.go @@ -15,13 +15,10 @@ package token import ( - "crypto" - "encoding/base64" - "encoding/json" - "fmt" "strings" "time" + "github.com/dgrijalva/jwt-go" "github.com/docker/distribution/registry/auth/token" "github.com/docker/libtrust" "github.com/goharbor/harbor/src/common/models" @@ -30,14 +27,17 @@ import ( "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/promgr" "github.com/goharbor/harbor/src/lib/log" + tokenpkg "github.com/goharbor/harbor/src/pkg/token" + "github.com/goharbor/harbor/src/pkg/token/claims/v2" ) const ( - // Issuer is the issuer of the internal token service in Harbor for registry - Issuer = "harbor-token-issuer" + signingMethod = "RS256" ) -var privateKey string +var ( + privateKey string +) func init() { privateKey = config.TokenPrivateKeyPath() @@ -56,7 +56,7 @@ func GetResourceActions(scopes []string) []*token.ResourceActions { typee := "" name := "" - actions := []string{} + actions := make([]string, 0) if length == 1 { typee = items[0] @@ -104,7 +104,7 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context, // MakeToken makes a valid jwt token based on parms. func MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) { - pk, err := libtrust.LoadKeyFile(privateKey) + options, err := tokenpkg.NewOptions(signingMethod, v2.Issuer, privateKey) if err != nil { return nil, err } @@ -112,21 +112,45 @@ func MakeToken(username, service string, access []*token.ResourceActions) (*mode if err != nil { return nil, err } + now := time.Now().UTC() - tk, expiresIn, issuedAt, err := makeTokenCore(Issuer, username, service, expiration, access, pk) + claims := &v2.Claims{ + StandardClaims: jwt.StandardClaims{ + Issuer: options.Issuer, + Subject: username, + Audience: service, + ExpiresAt: now.Add(time.Duration(expiration) * time.Minute).Unix(), + NotBefore: now.Unix(), + IssuedAt: now.Unix(), + Id: utils.GenerateRandomStringWithLen(16), + }, + Access: access, + } + tok, err := tokenpkg.New(options, claims) + if err != nil { + return nil, err + } + // Add kid to token header for compatibility with docker distribution's code + // see https://github.com/docker/distribution/blob/release/2.7/registry/auth/token/token.go#L197 + k, err := libtrust.UnmarshalPrivateKeyPEM(options.PrivateKey) + if err != nil { + return nil, err + } + tok.Header["kid"] = k.KeyID() + + rawToken, err := tok.Raw() if err != nil { return nil, err } - rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature)) return &models.Token{ - Token: rs, - ExpiresIn: expiresIn, - IssuedAt: issuedAt.Format(time.RFC3339), + Token: rawToken, + ExpiresIn: expiration * 60, + IssuedAt: now.Format(time.RFC3339), }, nil } func permToActions(p string) []string { - res := []string{} + res := make([]string, 0) if strings.Contains(p, "W") { res = append(res, "push") } @@ -141,58 +165,3 @@ func permToActions(p string) []string { } return res } - -// make token core -func makeTokenCore(issuer, subject, audience string, expiration int, - access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) { - - joseHeader := &token.Header{ - Type: "JWT", - SigningAlg: "RS256", - KeyID: signingKey.KeyID(), - } - - jwtID := utils.GenerateRandomStringWithLen(16) - - now := time.Now().UTC() - issuedAt = &now - expiresIn = expiration * 60 - - claimSet := &token.ClaimSet{ - Issuer: issuer, - Subject: subject, - Audience: audience, - Expiration: now.Add(time.Duration(expiration) * time.Minute).Unix(), - NotBefore: now.Unix(), - IssuedAt: now.Unix(), - JWTID: jwtID, - Access: access, - } - - var joseHeaderBytes, claimSetBytes []byte - - if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil { - return nil, 0, nil, fmt.Errorf("unable to marshal jose header: %s", err) - } - if claimSetBytes, err = json.Marshal(claimSet); err != nil { - return nil, 0, nil, fmt.Errorf("unable to marshal claim set: %s", err) - } - - encodedJoseHeader := base64UrlEncode(joseHeaderBytes) - encodedClaimSet := base64UrlEncode(claimSetBytes) - payload := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet) - - var signatureBytes []byte - if signatureBytes, _, err = signingKey.Sign(strings.NewReader(payload), crypto.SHA256); err != nil { - return nil, 0, nil, fmt.Errorf("unable to sign jwt payload: %s", err) - } - - signature := base64UrlEncode(signatureBytes) - tokenString := fmt.Sprintf("%s.%s", payload, signature) - t, err = token.NewToken(tokenString) - return -} - -func base64UrlEncode(b []byte) string { - return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") -} diff --git a/src/core/service/token/token_test.go b/src/core/service/token/token_test.go index ec4124ac5..6651dc194 100644 --- a/src/core/service/token/token_test.go +++ b/src/core/service/token/token_test.go @@ -136,14 +136,13 @@ func TestMakeToken(t *testing.T) { t.Errorf("Error while making token: %v", err) } tokenString := tokenJSON.Token - // t.Logf("privatekey: %s, crt: %s", tokenString, crt) pubKey, err := getPublicKey(crt) if err != nil { t.Errorf("Error while getting public key from cert: %s", crt) } tok, err := jwt.ParseWithClaims(tokenString, &harborClaims{}, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"]) + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return pubKey, nil }) @@ -162,7 +161,7 @@ func TestPermToActions(t *testing.T) { perm3 := "" expect1 := []string{"push", "*", "pull"} expect2 := []string{"*", "pull"} - expect3 := []string{} + expect3 := make([]string, 0) res1 := permToActions(perm1) res2 := permToActions(perm2) res3 := permToActions(perm3) diff --git a/src/pkg/token/claims/v2/claims.go b/src/pkg/token/claims/v2/claims.go new file mode 100644 index 000000000..06498fab8 --- /dev/null +++ b/src/pkg/token/claims/v2/claims.go @@ -0,0 +1,30 @@ +package v2 + +import ( + "fmt" + + "github.com/dgrijalva/jwt-go" + "github.com/docker/distribution/registry/auth/token" +) + +const ( + // Issuer is the only valid issuer for jwt token sent to /v2/xxxx + Issuer = "harbor-token-issuer" +) + +// Claims represents the token claims that encapsulated in a JWT token for registry/notary resources +type Claims struct { + jwt.StandardClaims + Access []*token.ResourceActions `json:"access"` +} + +// Valid checks if the issuer is harbor +func (c *Claims) Valid() error { + if err := c.StandardClaims.Valid(); err != nil { + return err + } + if !c.VerifyIssuer(Issuer, true) { + return fmt.Errorf("invalid token issuer: %s", c.Issuer) + } + return nil +} diff --git a/src/pkg/token/claims/v2/claims_test.go b/src/pkg/token/claims/v2/claims_test.go new file mode 100644 index 000000000..968378d0a --- /dev/null +++ b/src/pkg/token/claims/v2/claims_test.go @@ -0,0 +1,43 @@ +package v2 + +import ( + "testing" + + "github.com/dgrijalva/jwt-go" + "github.com/docker/distribution/registry/auth/token" + "github.com/stretchr/testify/assert" +) + +func TestValid(t *testing.T) { + cases := []struct { + claims Claims + valid bool + }{ + { + claims: Claims{ + StandardClaims: jwt.StandardClaims{ + Issuer: "anonymous", + }, + Access: []*token.ResourceActions{}, + }, + valid: false, + }, + { + claims: Claims{ + StandardClaims: jwt.StandardClaims{ + Issuer: Issuer, + }, + Access: []*token.ResourceActions{}, + }, + valid: true, + }, + } + + for _, tc := range cases { + if tc.valid { + assert.Nil(t, tc.claims.Valid()) + } else { + assert.NotNil(t, tc.claims.Valid()) + } + } +} diff --git a/src/pkg/token/option_test.go b/src/pkg/token/option_test.go index 421bf0cbb..3ff082c99 100644 --- a/src/pkg/token/option_test.go +++ b/src/pkg/token/option_test.go @@ -1,10 +1,10 @@ package token import ( + "testing" + "github.com/dgrijalva/jwt-go" "github.com/stretchr/testify/assert" - "testing" - "time" ) func TestNewOptions(t *testing.T) { @@ -12,7 +12,6 @@ func TestNewOptions(t *testing.T) { assert.NotNil(t, defaultOpt) assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256")) assert.Equal(t, defaultOpt.Issuer, "harbor-token-defaultIssuer") - assert.Equal(t, defaultOpt.TTL, 60*time.Minute) } func TestGetKey(t *testing.T) { diff --git a/src/pkg/token/options.go b/src/pkg/token/options.go index 1d8d2602d..e05a33065 100644 --- a/src/pkg/token/options.go +++ b/src/pkg/token/options.go @@ -3,15 +3,14 @@ package token import ( "crypto/rsa" "fmt" + "io/ioutil" + "github.com/dgrijalva/jwt-go" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/lib/log" - "io/ioutil" - "time" ) const ( - defaultTTL = 60 * time.Minute defaultIssuer = "harbor-token-defaultIssuer" defaultSignedMethod = "RS256" ) @@ -21,7 +20,6 @@ type Options struct { SignMethod jwt.SigningMethod PublicKey []byte PrivateKey []byte - TTL time.Duration Issuer string } @@ -62,17 +60,20 @@ func (o *Options) GetKey() (interface{}, error) { // DefaultTokenOptions ... func DefaultTokenOptions() *Options { - privateKeyFile := config.TokenPrivateKeyPath() - privateKey, err := ioutil.ReadFile(privateKeyFile) - if err != nil { - log.Errorf(fmt.Sprintf("failed to read private key %v", err)) - return nil - } - opt := &Options{ - SignMethod: jwt.GetSigningMethod(defaultSignedMethod), - PrivateKey: privateKey, - Issuer: defaultIssuer, - TTL: defaultTTL, - } + opt, _ := NewOptions(defaultSignedMethod, defaultIssuer, config.TokenPrivateKeyPath()) return opt } + +// NewOptions create Options based on input parms +func NewOptions(sm, iss, keyPath string) (*Options, error) { + pk, err := ioutil.ReadFile(keyPath) + if err != nil { + log.Errorf(fmt.Sprintf("failed to read private key %v", err)) + return nil, err + } + return &Options{ + PrivateKey: pk, + SignMethod: jwt.GetSigningMethod(sm), + Issuer: iss, + }, nil +} diff --git a/src/server/middleware/security/v2_token.go b/src/server/middleware/security/v2_token.go index c525847a1..27c99238c 100644 --- a/src/server/middleware/security/v2_token.go +++ b/src/server/middleware/security/v2_token.go @@ -5,30 +5,27 @@ import ( "net/http" "strings" - "github.com/dgrijalva/jwt-go" registry_token "github.com/docker/distribution/registry/auth/token" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security/v2token" svc_token "github.com/goharbor/harbor/src/core/service/token" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/pkg/token" + v2 "github.com/goharbor/harbor/src/pkg/token/claims/v2" ) type v2TokenClaims struct { - jwt.StandardClaims + v2.Claims Access []*registry_token.ResourceActions `json:"access"` } func (vtc *v2TokenClaims) Valid() error { - if err := vtc.StandardClaims.Valid(); err != nil { + if err := vtc.Claims.Valid(); err != nil { return err } if !vtc.VerifyAudience(svc_token.Registry, true) { return fmt.Errorf("invalid token audience: %s", vtc.Audience) } - if !vtc.VerifyIssuer(svc_token.Issuer, true) { - return fmt.Errorf("invalid token issuer: %s", vtc.Issuer) - } return nil }