golang-client/identity/middleware/validation.go

345 lines
9.0 KiB
Go

// Copyright (c) 2016 eBay Inc.
//
// 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 middleware
import (
"bytes"
"compress/zlib"
"crypto/md5"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
"git.openstack.org/openstack/golang-client/openstack"
"github.com/fullsailor/pkcs7"
)
const (
PKI_ASN1_PREFIX = "MII"
PKIZ_PREFIX = "PKIZ_"
)
// Cache the token until it gets expired
var serviceTokenSession *openstack.Session
// Cache token revocation list until expired
var revocationListCache *revokedListCache
// NewValidator gets the credential for service account, token need to be validated,
// signing cert location (will store the cert from keystone if not there), and the
// revocation list cache duration (in seconds) and returns the validator.
func NewValidator(authOpts openstack.AuthOpts, token string, signingKeyPath string, revCacheSecs int) *Validator {
return &Validator{
SvcAuthOpts: authOpts,
CachedSigningKeyPath: signingKeyPath,
TokenId: token,
RevCacheDuration: time.Duration(revCacheSecs) * time.Second,
}
}
// Validate does the local validation for PKI & PKIZ token and sends to keystone
// for other format tokens validation. It returns the extracted AuthToken struct
func (validator *Validator) Validate() (*openstack.AuthToken, error) {
var token *openstack.AuthToken
if strings.HasPrefix(validator.TokenId, PKIZ_PREFIX) ||
strings.HasPrefix(validator.TokenId, PKI_ASN1_PREFIX) {
// do local validation for PKI and PKIZ token
if revocationListCache == nil || time.Now().Sub(revocationListCache.Time) > validator.RevCacheDuration {
_, err := validator.getRevocationList()
if err != nil {
return nil, err
}
}
access, err := validator.ValidateOffline()
if err != nil {
return nil, err
}
if err = json.Unmarshal(access, &token); err != nil {
return nil, err
}
// set the token ID
token.Access.Token.ID = validator.TokenId
hashedTokenId := fmt.Sprintf("%x", md5.Sum([]byte(validator.TokenId)))
for _, rtoken := range revocationListCache.Revoked {
if rtoken.ID == hashedTokenId {
return nil, fmt.Errorf("token %s was revoked", hashedTokenId)
}
}
} else {
access, err := validator.ValidateRemote()
if err != nil {
return nil, err
}
if err = json.Unmarshal(access, &token); err != nil {
return nil, err
}
}
// validation should fail if token is expired
if token.Access.Token.Expires.Sub(time.Now()) < 0 {
return nil, fmt.Errorf("token %s is expired", validator.TokenId)
}
return token, nil
}
// Validate the token locally without sending to keystone
// It take the token body and return the extracted access object as []byte
func (validator *Validator) ValidateOffline() ([]byte, error) {
token := ""
switch {
case strings.HasPrefix(validator.TokenId, PKIZ_PREFIX):
token = strings.TrimPrefix(validator.TokenId, PKIZ_PREFIX)
decompressedToken, err := decompressToken(token)
if err != nil {
return nil, err
}
token = trimCMSFormat(decompressedToken)
case strings.HasPrefix(validator.TokenId, PKI_ASN1_PREFIX):
token = validator.TokenId
default:
return nil, errors.New("can not validate offline, it has to be sent to keystone")
}
decodedToken, err := base64DecodeFromCms(token)
if err != nil {
return nil, err
}
content, err := validator.checkSignature(decodedToken)
if err != nil {
return nil, err
}
return content, nil
}
func (validator *Validator) ValidateRemote() ([]byte, error) {
resp, err := validator.reqTokenValidation()
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
// retry one more time using the new service token
err = validator.renewToken()
if err != nil {
return nil, err
}
resp, err = validator.reqTokenValidation()
if err != nil {
return nil, err
}
}
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.New("error reading response body")
}
return rbody, nil
}
func decompressToken(token string) (string, error) {
decToken, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return "", err
}
zr, err := zlib.NewReader(bytes.NewBuffer(decToken))
if err != nil {
return "", err
}
bb, err := ioutil.ReadAll(zr)
if err != nil {
return "", err
}
return string(bb), nil
}
func base64DecodeFromCms(token string) ([]byte, error) {
t := strings.Replace(token, "-", "/", -1)
decToken, err := base64.StdEncoding.DecodeString(t)
if err != nil {
return nil, err
}
return decToken, nil
}
// remove the customerized header and footer in PEM token
// -----BEGIN CMS-----
// -----END CMS-----
func trimCMSFormat(token string) string {
token = strings.Trim(token, "\n")
l := strings.Index(token, "\n")
r := strings.LastIndex(token, "\n")
return token[l:r]
}
// Get the signging certificate from local dir
// It will get the cert from keystone if cache file does not exist
func (validator *Validator) getSigningCert() (*x509.Certificate, error) {
signPEM, err := ioutil.ReadFile(validator.CachedSigningKeyPath)
if err != nil {
resp, err := http.Get(validator.SvcAuthOpts.AuthUrl + "/certificates/signing")
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, errors.New("can not get signing cert")
}
signPEM, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// cache the file to location
if err = ioutil.WriteFile(validator.CachedSigningKeyPath, []byte(signPEM), 0644); err != nil {
log.Println("error caching signging cert")
}
}
block, _ := pem.Decode(signPEM)
if block == nil {
return nil, errors.New("can not decode PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return cert, nil
}
// check the signature of the token
func (validator *Validator) checkSignature(data []byte) ([]byte, error) {
p7, err := pkcs7.Parse(data)
if err != nil {
return nil, err
}
if len(p7.Signers) != 1 {
return nil, errors.New("should be only one signature found")
}
signer := p7.Signers[0]
cert, err := validator.getSigningCert()
if err != nil {
return nil, err
}
err = cert.CheckSignature(x509.SHA256WithRSA, p7.Content, signer.EncryptedDigest)
if err != nil {
return nil, err
}
return p7.Content, nil
}
// getRevocationList get a list of revoked tokens
func (validator *Validator) getRevocationList() ([]openstack.Token, error) {
// Get the service token to get the token revocation list
resp, err := validator.reqRevocationList()
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
// try again when getting 401, it could be the cached token was revoked
// or the token got expired
validator.renewToken()
resp, err = validator.reqRevocationList()
if err != nil {
return nil, err
}
}
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
revokeCMSResp := revokeResp{}
if err = json.Unmarshal(rbody, &revokeCMSResp); err != nil {
return nil, err
}
revokeResp := trimCMSFormat(revokeCMSResp.Signed)
decodedResp, err := base64DecodeFromCms(revokeResp)
if err != nil {
return nil, err
}
revoked, err := validator.checkSignature(decodedResp)
if err != nil {
return nil, err
}
revokedList := RevokedList{}
if err = json.Unmarshal(revoked, &revokedList); err != nil {
return nil, err
}
// update the revocation list cache
revocationListCache = &revokedListCache{
Revoked: revokedList.Revoked,
Time: time.Now(),
}
return revokedList.Revoked, nil
}
// reqRevocationList sends the GET request to keystone and get response back
func (validator *Validator) reqRevocationList() (*http.Response, error) {
if serviceTokenSession == nil {
validator.renewToken()
}
// Get token revocation list
return serviceTokenSession.Request("GET", validator.SvcAuthOpts.AuthUrl+"/tokens/revoked", nil, nil, nil)
}
func (validator *Validator) reqTokenValidation() (*http.Response, error) {
if serviceTokenSession == nil {
validator.renewToken()
}
// Get token revocation list
reqUrl := fmt.Sprintf("%s/tokens/%s", validator.SvcAuthOpts.AuthUrl, validator.TokenId)
return serviceTokenSession.Request("GET", reqUrl, nil, nil, nil)
}
// renewToken gets the keystone token from service AuthOpts
func (validator *Validator) renewToken() error {
// Get the service token to get the token revocation list
auth, err := openstack.DoAuthRequest(validator.SvcAuthOpts)
if err != nil {
return err
}
// Make a new client with these creds
serviceTokenSession, err = openstack.NewSession(nil, auth, nil)
if err != nil {
return err
}
return nil
}