Merge pull request #1300 from hashicorp/aws-auth-backend

AWS EC2 instances authentication backend
This commit is contained in:
Vishal Nayak 2016-05-14 19:42:03 -04:00
commit 943789a11e
29 changed files with 7373 additions and 30 deletions

View file

@ -0,0 +1,177 @@
package aws
import (
"sync"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
b, err := Backend(conf)
if err != nil {
return nil, err
}
return b.Setup(conf)
}
type backend struct {
*framework.Backend
Salt *salt.Salt
// Lock to make changes to any of the backend's configuration endpoints.
configMutex sync.RWMutex
// Lock to make changes to role entries
roleMutex sync.RWMutex
// Lock to make changes to the blacklist entries
blacklistMutex sync.RWMutex
// Guards the blacklist/whitelist tidy functions
tidyBlacklistCASGuard uint32
tidyWhitelistCASGuard uint32
// Duration after which the periodic function of the backend needs to
// tidy the blacklist and whitelist entries.
tidyCooldownPeriod time.Duration
// nextTidyTime holds the time at which the periodic func should initiatite
// the tidy operations. This is set by the periodicFunc based on the value
// of tidyCooldownPeriod.
nextTidyTime time.Time
// Map to hold the EC2 client objects indexed by region. This avoids the
// overhead of creating a client object for every login request. When
// the credentials are modified or deleted, all the cached client objects
// will be flushed.
EC2ClientsMap map[string]*ec2.EC2
}
func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
salt, err := salt.NewSalt(conf.StorageView, &salt.Config{
HashFunc: salt.SHA256Hash,
})
if err != nil {
return nil, err
}
b := &backend{
// Setting the periodic func to be run once in an hour.
// If there is a real need, this can be made configurable.
tidyCooldownPeriod: time.Hour,
Salt: salt,
EC2ClientsMap: make(map[string]*ec2.EC2),
}
b.Backend = &framework.Backend{
PeriodicFunc: b.periodicFunc,
AuthRenew: b.pathLoginRenew,
Help: backendHelp,
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"login",
},
},
Paths: []*framework.Path{
pathLogin(b),
pathListRole(b),
pathListRoles(b),
pathRole(b),
pathRoleTag(b),
pathConfigClient(b),
pathConfigCertificate(b),
pathConfigTidyRoletagBlacklist(b),
pathConfigTidyIdentityWhitelist(b),
pathListCertificates(b),
pathListRoletagBlacklist(b),
pathRoletagBlacklist(b),
pathTidyRoletagBlacklist(b),
pathListIdentityWhitelist(b),
pathIdentityWhitelist(b),
pathTidyIdentityWhitelist(b),
},
}
return b.Backend, nil
}
// periodicFunc performs the tasks that the backend wishes to do periodically.
// Currently this will be triggered once in a minute by the RollbackManager.
//
// The tasks being done currently by this function are to cleanup the expired
// entries of both blacklist role tags and whitelist identities. Tidying is done
// not once in a minute, but once in an hour, controlled by 'tidyCooldownPeriod'.
// Tidying of blacklist and whitelist are by default enabled. This can be
// changed using `config/tidy/roletags` and `config/tidy/identities` endpoints.
func (b *backend) periodicFunc(req *logical.Request) error {
// Run the tidy operations for the first time. Then run it when current
// time matches the nextTidyTime.
if b.nextTidyTime.IsZero() || !time.Now().UTC().Before(b.nextTidyTime) {
// safety_buffer defaults to 180 days for roletag blacklist
safety_buffer := 15552000
tidyBlacklistConfigEntry, err := b.configTidyRoleTags(req.Storage)
if err != nil {
return err
}
skipBlacklistTidy := false
// check if tidying of role tags was configured
if tidyBlacklistConfigEntry != nil {
// check if periodic tidying of role tags was disabled
if tidyBlacklistConfigEntry.DisablePeriodicTidy {
skipBlacklistTidy = true
}
// overwrite the default safety_buffer with the configured value
safety_buffer = tidyBlacklistConfigEntry.SafetyBuffer
}
// tidy role tags if explicitly not disabled
if !skipBlacklistTidy {
b.tidyBlacklistRoleTag(req.Storage, safety_buffer)
}
// reset the safety_buffer to 72h
safety_buffer = 259200
tidyWhitelistConfigEntry, err := b.configTidyIdentities(req.Storage)
if err != nil {
return err
}
skipWhitelistTidy := false
// check if tidying of identities was configured
if tidyWhitelistConfigEntry != nil {
// check if periodic tidying of identities was disabled
if tidyWhitelistConfigEntry.DisablePeriodicTidy {
skipWhitelistTidy = true
}
// overwrite the default safety_buffer with the configured value
safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer
}
// tidy identities if explicitly not disabled
if !skipWhitelistTidy {
b.tidyWhitelistIdentity(req.Storage, safety_buffer)
}
// Update the time at which to run the tidy functions again.
b.nextTidyTime = time.Now().UTC().Add(b.tidyCooldownPeriod)
}
return nil
}
const backendHelp = `
AWS auth backend takes in PKCS#7 signature of an AWS EC2 instance and a client
created nonce to authenticates the EC2 instance with Vault.
Authentication is backed by a preconfigured role in the backend. The role
represents the authorization of resources by containing Vault's policies.
Role can be created using 'role/<role>' endpoint.
If there is need to further restrict the capabilities of the role on the instance
that is using the role, 'role_tag' option can be enabled on the role, and a tag
can be generated using 'role/<role>/tag' endpoint. This tag represents the
subset of capabilities set on the role. When the 'role_tag' option is enabled on
the role, the login operation requires that a respective role tag is attached to
the EC2 instance which performs the login.
`

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,103 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/helper/awsutil"
"github.com/hashicorp/vault/logical"
)
// getClientConfig creates a aws-sdk-go config, which is used to create client
// that can interact with AWS API. This builds credentials in the following
// order of preference:
//
// * Static credentials from 'config/client'
// * Environment variables
// * Instance metadata role
func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config, error) {
credsConfig := &awsutil.CredentialsConfig{
Region: region,
}
// Read the configured secret key and access key
config, err := b.clientConfigEntryInternal(s)
if err != nil {
return nil, err
}
endpoint := aws.String("")
if config != nil {
// Override the default endpoint with the configured endpoint.
if config.Endpoint != "" {
endpoint = aws.String(config.Endpoint)
}
credsConfig.AccessKey = config.AccessKey
credsConfig.SecretKey = config.SecretKey
}
credsConfig.HTTPClient = cleanhttp.DefaultClient()
creds, err := credsConfig.GenerateCredentialChain()
if err != nil {
return nil, err
}
if creds == nil {
return nil, fmt.Errorf("could not compile valid credential providers from static config, environemnt, shared, or instance metadata")
}
// Create a config that can be used to make the API calls.
return &aws.Config{
Credentials: creds,
Region: aws.String(region),
HTTPClient: cleanhttp.DefaultClient(),
Endpoint: endpoint,
}, nil
}
// flushCachedEC2Clients deletes all the cached ec2 client objects from the backend.
// If the client credentials configuration is deleted or updated in the backend, all
// the cached EC2 client objects will be flushed.
//
// Write lock should be acquired using b.configMutex.Lock() before calling this method
// and lock should be released using b.configMutex.Unlock() after the method returns.
func (b *backend) flushCachedEC2Clients() {
// deleting items in map during iteration is safe.
for region, _ := range b.EC2ClientsMap {
delete(b.EC2ClientsMap, region)
}
}
// clientEC2 creates a client to interact with AWS EC2 API.
func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error) {
b.configMutex.RLock()
if b.EC2ClientsMap[region] != nil {
defer b.configMutex.RUnlock()
// If the client object was already created, return it.
return b.EC2ClientsMap[region], nil
}
// Release the read lock and acquire the write lock.
b.configMutex.RUnlock()
b.configMutex.Lock()
defer b.configMutex.Unlock()
// If the client gets created while switching the locks, return it.
if b.EC2ClientsMap[region] != nil {
return b.EC2ClientsMap[region], nil
}
// Create a AWS config object using a chain of providers.
awsConfig, err := b.getClientConfig(s, region)
if err != nil {
return nil, err
}
// Create a new EC2 client object, cache it and return the same.
b.EC2ClientsMap[region] = ec2.New(session.New(awsConfig))
return b.EC2ClientsMap[region], nil
}

View file

@ -0,0 +1,334 @@
package aws
import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// dsaSignature represents the contents of the signature of a signed
// content using digital signature algorithm.
type dsaSignature struct {
R, S *big.Int
}
// As per AWS documentation, this public key is valid for US East (N. Virginia),
// US West (Oregon), US West (N. California), EU (Ireland), EU (Frankfurt),
// Asia Pacific (Tokyo), Asia Pacific (Seoul), Asia Pacific (Singapore),
// Asia Pacific (Sydney), and South America (Sao Paulo).
//
// It's also the same certificate, but for some reason listed separately, for
// GovCloud (US)
const genericAWSPublicCertificate = `-----BEGIN CERTIFICATE-----
MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw
FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD
VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z
ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u
IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl
cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e
ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3
VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P
hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j
k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U
hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF
lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf
MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW
MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw
vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw
7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K
-----END CERTIFICATE-----
`
// pathListCertificates creates a path that enables listing of all
// the AWS public certificates registered with Vault.
func pathListCertificates(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/certificates/?",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathCertificatesList,
},
HelpSynopsis: pathListCertificatesHelpSyn,
HelpDescription: pathListCertificatesHelpDesc,
}
}
func pathConfigCertificate(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/certificate/" + framework.GenericNameRegex("cert_name"),
Fields: map[string]*framework.FieldSchema{
"cert_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the certificate.",
},
"aws_public_cert": &framework.FieldSchema{
Type: framework.TypeString,
Description: "AWS Public cert required to verify PKCS7 signature of the EC2 instance metadata.",
},
},
ExistenceCheck: b.pathConfigCertificateExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigCertificateCreateUpdate,
logical.UpdateOperation: b.pathConfigCertificateCreateUpdate,
logical.ReadOperation: b.pathConfigCertificateRead,
logical.DeleteOperation: b.pathConfigCertificateDelete,
},
HelpSynopsis: pathConfigCertificateSyn,
HelpDescription: pathConfigCertificateDesc,
}
}
// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
func (b *backend) pathConfigCertificateExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
certName := data.Get("cert_name").(string)
if certName == "" {
return false, fmt.Errorf("missing cert_name")
}
entry, err := b.awsPublicCertificateEntry(req.Storage, certName)
if err != nil {
return false, err
}
return entry != nil, nil
}
// pathCertificatesList is used to list all the AWS public certificates registered with Vault.
func (b *backend) pathCertificatesList(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
certs, err := req.Storage.List("config/certificate/")
if err != nil {
return nil, err
}
return logical.ListResponse(certs), nil
}
// Decodes the PEM encoded certiticate and parses it into a x509 cert.
func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) {
// Decode the PEM block and error out if a block is not detected in the first attempt.
decodedPublicCert, rest := pem.Decode([]byte(certificate))
if len(rest) != 0 {
return nil, fmt.Errorf("invalid certificate; should be one PEM block only")
}
// Check if the certificate can be parsed.
publicCert, err := x509.ParseCertificate(decodedPublicCert.Bytes)
if err != nil {
return nil, err
}
if publicCert == nil {
return nil, fmt.Errorf("invalid certificate; failed to parse certificate")
}
return publicCert, nil
}
// awsPublicCertificates returns a slice of all the parsed AWS public
// certificates, that were registered using `config/certificate/<cert_name>` endpoint.
// This method will also append default certificate in the backend, to the slice.
func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) {
// Lock at beginning and use internal method so that we are consistent as
// we iterate through
b.configMutex.RLock()
defer b.configMutex.RUnlock()
var certs []*x509.Certificate
// Append the generic certificate provided in the AWS EC2 instance metadata documentation.
decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate)
if err != nil {
return nil, err
}
certs = append(certs, decodedCert)
// Get the list of all the registered certificates.
registeredCerts, err := s.List("config/certificate/")
if err != nil {
return nil, err
}
// Iterate through each certificate, parse and append it to a slice.
for _, cert := range registeredCerts {
certEntry, err := b.awsPublicCertificateEntryInternal(s, cert)
if err != nil {
return nil, err
}
if certEntry == nil {
return nil, fmt.Errorf("certificate storage has a nil entry under the name:%s\n", cert)
}
decodedCert, err := decodePEMAndParseCertificate(certEntry.AWSPublicCert)
if err != nil {
return nil, err
}
certs = append(certs, decodedCert)
}
return certs, nil
}
// awsPublicCertificate is used to get the configured AWS Public Key that is used
// to verify the PKCS#7 signature of the instance identity document.
func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
return b.awsPublicCertificateEntryInternal(s, certName)
}
// Internal version of the above that does no locking
func (b *backend) awsPublicCertificateEntryInternal(s logical.Storage, certName string) (*awsPublicCert, error) {
entry, err := s.Get("config/certificate/" + certName)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result awsPublicCert
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
// pathConfigCertificateDelete is used to delete the previously configured AWS Public Key
// that is used to verify the PKCS#7 signature of the instance identity document.
func (b *backend) pathConfigCertificateDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
certName := data.Get("cert_name").(string)
if certName == "" {
return logical.ErrorResponse("missing cert_name"), nil
}
return nil, req.Storage.Delete("config/certificate/" + certName)
}
// pathConfigCertificateRead is used to view the configured AWS Public Key that is
// used to verify the PKCS#7 signature of the instance identity document.
func (b *backend) pathConfigCertificateRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
certName := data.Get("cert_name").(string)
if certName == "" {
return logical.ErrorResponse("missing cert_name"), nil
}
certificateEntry, err := b.awsPublicCertificateEntry(req.Storage, certName)
if err != nil {
return nil, err
}
if certificateEntry == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(certificateEntry).Map(),
}, nil
}
// pathConfigCertificateCreateUpdate is used to register an AWS Public Key that is
// used to verify the PKCS#7 signature of the instance identity document.
func (b *backend) pathConfigCertificateCreateUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
certName := data.Get("cert_name").(string)
if certName == "" {
return logical.ErrorResponse("missing cert_name"), nil
}
b.configMutex.Lock()
defer b.configMutex.Unlock()
// Check if there is already a certificate entry registered.
certEntry, err := b.awsPublicCertificateEntryInternal(req.Storage, certName)
if err != nil {
return nil, err
}
if certEntry == nil {
certEntry = &awsPublicCert{}
}
// Check if the value is provided by the client.
certStrData, ok := data.GetOk("aws_public_cert")
if ok {
if certBytes, err := base64.StdEncoding.DecodeString(certStrData.(string)); err == nil {
certEntry.AWSPublicCert = string(certBytes)
} else {
certEntry.AWSPublicCert = certStrData.(string)
}
} else {
// aws_public_cert should be supplied for both create and update operations.
// If it is not provided, throw an error.
return logical.ErrorResponse("missing aws_public_cert"), nil
}
// If explicitly set to empty string, error out.
if certEntry.AWSPublicCert == "" {
return logical.ErrorResponse("invalid aws_public_cert"), nil
}
// Verify the certificate by decoding it and parsing it.
publicCert, err := decodePEMAndParseCertificate(certEntry.AWSPublicCert)
if err != nil {
return nil, err
}
if publicCert == nil {
return logical.ErrorResponse("invalid certificate; failed to decode and parse certificate"), nil
}
// Ensure that we have not
// If none of the checks fail, save the provided certificate.
entry, err := logical.StorageEntryJSON("config/certificate/"+certName, certEntry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
// Struct awsPublicCert holds the AWS Public Key that is used to verify the PKCS#7 signature
// of the instnace identity document.
type awsPublicCert struct {
AWSPublicCert string `json:"aws_public_cert" structs:"aws_public_cert" mapstructure:"aws_public_cert"`
}
const pathConfigCertificateSyn = `
Adds the AWS Public Key that is used to verify the PKCS#7 signature of the identidy document.
`
const pathConfigCertificateDesc = `
AWS Public Key which is used to verify the PKCS#7 signature of the identity document,
varies by region. The public key(s) can be found in AWS EC2 instance metadata documentation.
The default key that is used to verify the signature is the one that is applicable for
following regions: US East (N. Virginia), US West (Oregon), US West (N. California),
EU (Ireland), EU (Frankfurt), Asia Pacific (Tokyo), Asia Pacific (Seoul), Asia Pacific (Singapore),
Asia Pacific (Sydney), and South America (Sao Paulo).
If the instances belongs to region other than the above, the public key(s) for the
corresponding regions should be registered using this endpoint. PKCS#7 is verified
using a collection of certificates containing the default certificate and all the
certificates that are registered using this endpoint.
`
const pathListCertificatesHelpSyn = `
Lists all the AWS public certificates that are registered with the backend.
`
const pathListCertificatesHelpDesc = `
Certificates will be listed by their respective names that were used during registration.
`

View file

@ -0,0 +1,199 @@
package aws
import (
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfigClient(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/client$",
Fields: map[string]*framework.FieldSchema{
"access_key": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "AWS Access key with permissions to query EC2 DescribeInstances API.",
},
"secret_key": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "AWS Secret key with permissions to query EC2 DescribeInstances API.",
},
"endpoint": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "URL to override the default generated endpoint for making AWS EC2 API calls.",
},
},
ExistenceCheck: b.pathConfigClientExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigClientCreateUpdate,
logical.UpdateOperation: b.pathConfigClientCreateUpdate,
logical.DeleteOperation: b.pathConfigClientDelete,
logical.ReadOperation: b.pathConfigClientRead,
},
HelpSynopsis: pathConfigClientHelpSyn,
HelpDescription: pathConfigClientHelpDesc,
}
}
// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
func (b *backend) pathConfigClientExistenceCheck(
req *logical.Request, data *framework.FieldData) (bool, error) {
entry, err := b.clientConfigEntry(req.Storage)
if err != nil {
return false, err
}
return entry != nil, nil
}
// Fetch the client configuration required to access the AWS API.
func (b *backend) clientConfigEntry(s logical.Storage) (*clientConfig, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
return b.clientConfigEntryInternal(s)
}
// Internal version that does no locking
func (b *backend) clientConfigEntryInternal(s logical.Storage) (*clientConfig, error) {
entry, err := s.Get("config/client")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result clientConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathConfigClientRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
clientConfig, err := b.clientConfigEntry(req.Storage)
if err != nil {
return nil, err
}
if clientConfig == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(clientConfig).Map(),
}, nil
}
func (b *backend) pathConfigClientDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
if err := req.Storage.Delete("config/client"); err != nil {
return nil, err
}
// Remove all the cached EC2 client objects in the backend.
b.flushCachedEC2Clients()
return nil, nil
}
// pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key'
// that can be used to interact with AWS EC2 API.
func (b *backend) pathConfigClientCreateUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
configEntry, err := b.clientConfigEntryInternal(req.Storage)
if err != nil {
return nil, err
}
if configEntry == nil {
configEntry = &clientConfig{}
}
changedCreds := false
accessKeyStr, ok := data.GetOk("access_key")
if ok {
if configEntry.AccessKey != accessKeyStr.(string) {
changedCreds = true
configEntry.AccessKey = accessKeyStr.(string)
}
} else if req.Operation == logical.CreateOperation {
// Use the default
configEntry.AccessKey = data.Get("access_key").(string)
}
secretKeyStr, ok := data.GetOk("secret_key")
if ok {
if configEntry.SecretKey != secretKeyStr.(string) {
changedCreds = true
configEntry.SecretKey = secretKeyStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.SecretKey = data.Get("secret_key").(string)
}
endpointStr, ok := data.GetOk("endpoint")
if ok {
if configEntry.Endpoint != endpointStr.(string) {
changedCreds = true
configEntry.Endpoint = endpointStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.Endpoint = data.Get("endpoint").(string)
}
// Since this endpoint supports both create operation and update operation,
// the error checks for access_key and secret_key not being set are not present.
// This allows calling this endpoint multiple times to provide the values.
// Hence, the readers of this endpoint should do the validation on
// the validation of keys before using them.
entry, err := logical.StorageEntryJSON("config/client", configEntry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
if changedCreds {
b.flushCachedEC2Clients()
}
return nil, nil
}
// Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to
// interact with the AWS EC2 API.
type clientConfig struct {
AccessKey string `json:"access_key" structs:"access_key" mapstructure:"access_key"`
SecretKey string `json:"secret_key" structs:"secret_key" mapstructure:"secret_key"`
Endpoint string `json:"endpoint" structs:"endpoint" mapstructure:"endpoint"`
}
const pathConfigClientHelpSyn = `
Configure the client credentials that are used to query instance details from AWS EC2 API.
`
const pathConfigClientHelpDesc = `
AWS auth backend makes DescribeInstances API call to retrieve information regarding
the instance that performs login. The aws_secret_key and aws_access_key registered with Vault should have the
permissions to make the API call.
`

View file

@ -0,0 +1,149 @@
package aws
import (
"fmt"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const (
identityWhitelistConfigPath = "config/tidy/identity-whitelist"
)
func pathConfigTidyIdentityWhitelist(b *backend) *framework.Path {
return &framework.Path{
Pattern: fmt.Sprintf("%s$", identityWhitelistConfigPath),
Fields: map[string]*framework.FieldSchema{
"safety_buffer": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 259200, //72h
Description: `The amount of extra time that must have passed beyond the identity's
expiration, before it is removed from the backend storage.`,
},
"disable_periodic_tidy": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "If set to 'true', disables the periodic tidying of the 'identity-whitelist/<instance_id>' entries.",
},
},
ExistenceCheck: b.pathConfigTidyIdentityWhitelistExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigTidyIdentityWhitelistCreateUpdate,
logical.UpdateOperation: b.pathConfigTidyIdentityWhitelistCreateUpdate,
logical.ReadOperation: b.pathConfigTidyIdentityWhitelistRead,
logical.DeleteOperation: b.pathConfigTidyIdentityWhitelistDelete,
},
HelpSynopsis: pathConfigTidyIdentityWhitelistHelpSyn,
HelpDescription: pathConfigTidyIdentityWhitelistHelpDesc,
}
}
func (b *backend) pathConfigTidyIdentityWhitelistExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
entry, err := b.configTidyIdentities(req.Storage)
if err != nil {
return false, err
}
return entry != nil, nil
}
func (b *backend) configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
return b.configTidyIdentitiesInternal(s)
}
func (b *backend) configTidyIdentitiesInternal(s logical.Storage) (*tidyWhitelistIdentityConfig, error) {
entry, err := s.Get(identityWhitelistConfigPath)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result tidyWhitelistIdentityConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathConfigTidyIdentityWhitelistCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
configEntry, err := b.configTidyIdentitiesInternal(req.Storage)
if err != nil {
return nil, err
}
if configEntry == nil {
configEntry = &tidyWhitelistIdentityConfig{}
}
safetyBufferInt, ok := data.GetOk("safety_buffer")
if ok {
configEntry.SafetyBuffer = safetyBufferInt.(int)
} else if req.Operation == logical.CreateOperation {
configEntry.SafetyBuffer = data.Get("safety_buffer").(int)
}
disablePeriodicTidyBool, ok := data.GetOk("disable_periodic_tidy")
if ok {
configEntry.DisablePeriodicTidy = disablePeriodicTidyBool.(bool)
} else if req.Operation == logical.CreateOperation {
configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool)
}
entry, err := logical.StorageEntryJSON(identityWhitelistConfigPath, configEntry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathConfigTidyIdentityWhitelistRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
clientConfig, err := b.configTidyIdentities(req.Storage)
if err != nil {
return nil, err
}
if clientConfig == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(clientConfig).Map(),
}, nil
}
func (b *backend) pathConfigTidyIdentityWhitelistDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
return nil, req.Storage.Delete(identityWhitelistConfigPath)
}
type tidyWhitelistIdentityConfig struct {
SafetyBuffer int `json:"safety_buffer" structs:"safety_buffer" mapstructure:"safety_buffer"`
DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"`
}
const pathConfigTidyIdentityWhitelistHelpSyn = `
Configures the periodic tidying operation of the whitelisted identity entries.
`
const pathConfigTidyIdentityWhitelistHelpDesc = `
By default, the expired entries in the whitelist will be attempted to be removed
periodically. This operation will look for expired items in the list and purges them.
However, there is a safety buffer duration (defaults to 72h), purges the entries
only if they have been persisting this duration, past its expiration time.
`

View file

@ -0,0 +1,150 @@
package aws
import (
"fmt"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const (
roletagBlacklistConfigPath = "config/tidy/roletag-blacklist"
)
func pathConfigTidyRoletagBlacklist(b *backend) *framework.Path {
return &framework.Path{
Pattern: fmt.Sprintf("%s$", roletagBlacklistConfigPath),
Fields: map[string]*framework.FieldSchema{
"safety_buffer": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 15552000, //180d
Description: `The amount of extra time that must have passed beyond the roletag
expiration, before it is removed from the backend storage.
Defaults to 4320h (180 days).`,
},
"disable_periodic_tidy": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "If set to 'true', disables the periodic tidying of blacklisted entries.",
},
},
ExistenceCheck: b.pathConfigTidyRoletagBlacklistExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigTidyRoletagBlacklistCreateUpdate,
logical.UpdateOperation: b.pathConfigTidyRoletagBlacklistCreateUpdate,
logical.ReadOperation: b.pathConfigTidyRoletagBlacklistRead,
logical.DeleteOperation: b.pathConfigTidyRoletagBlacklistDelete,
},
HelpSynopsis: pathConfigTidyRoletagBlacklistHelpSyn,
HelpDescription: pathConfigTidyRoletagBlacklistHelpDesc,
}
}
func (b *backend) pathConfigTidyRoletagBlacklistExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
entry, err := b.configTidyRoleTags(req.Storage)
if err != nil {
return false, err
}
return entry != nil, nil
}
func (b *backend) configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
return b.configTidyRoleTagsInternal(s)
}
func (b *backend) configTidyRoleTagsInternal(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) {
entry, err := s.Get(roletagBlacklistConfigPath)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result tidyBlacklistRoleTagConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathConfigTidyRoletagBlacklistCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
configEntry, err := b.configTidyRoleTagsInternal(req.Storage)
if err != nil {
return nil, err
}
if configEntry == nil {
configEntry = &tidyBlacklistRoleTagConfig{}
}
safetyBufferInt, ok := data.GetOk("safety_buffer")
if ok {
configEntry.SafetyBuffer = safetyBufferInt.(int)
} else if req.Operation == logical.CreateOperation {
configEntry.SafetyBuffer = data.Get("safety_buffer").(int)
}
disablePeriodicTidyBool, ok := data.GetOk("disable_periodic_tidy")
if ok {
configEntry.DisablePeriodicTidy = disablePeriodicTidyBool.(bool)
} else if req.Operation == logical.CreateOperation {
configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool)
}
entry, err := logical.StorageEntryJSON(roletagBlacklistConfigPath, configEntry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathConfigTidyRoletagBlacklistRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
clientConfig, err := b.configTidyRoleTags(req.Storage)
if err != nil {
return nil, err
}
if clientConfig == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(clientConfig).Map(),
}, nil
}
func (b *backend) pathConfigTidyRoletagBlacklistDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
return nil, req.Storage.Delete(roletagBlacklistConfigPath)
}
type tidyBlacklistRoleTagConfig struct {
SafetyBuffer int `json:"safety_buffer" structs:"safety_buffer" mapstructure:"safety_buffer"`
DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"`
}
const pathConfigTidyRoletagBlacklistHelpSyn = `
Configures the periodic tidying operation of the blacklisted role tag entries.
`
const pathConfigTidyRoletagBlacklistHelpDesc = `
By default, the expired entries in the blacklist will be attempted to be removed
periodically. This operation will look for expired items in the list and purges them.
However, there is a safety buffer duration (defaults to 72h), purges the entries
only if they have been persisting this duration, past its expiration time.
`

View file

@ -0,0 +1,154 @@
package aws
import (
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathIdentityWhitelist(b *backend) *framework.Path {
return &framework.Path{
Pattern: "identity-whitelist/" + framework.GenericNameRegex("instance_id"),
Fields: map[string]*framework.FieldSchema{
"instance_id": &framework.FieldSchema{
Type: framework.TypeString,
Description: `EC2 instance ID. A successful login operation from an EC2 instance
gets cached in this whitelist, keyed off of instance ID.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathIdentityWhitelistRead,
logical.DeleteOperation: b.pathIdentityWhitelistDelete,
},
HelpSynopsis: pathIdentityWhitelistSyn,
HelpDescription: pathIdentityWhitelistDesc,
}
}
func pathListIdentityWhitelist(b *backend) *framework.Path {
return &framework.Path{
Pattern: "identity-whitelist/?",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathWhitelistIdentitiesList,
},
HelpSynopsis: pathListIdentityWhitelistHelpSyn,
HelpDescription: pathListIdentityWhitelistHelpDesc,
}
}
// pathWhitelistIdentitiesList is used to list all the instance IDs that are present
// in the identity whitelist. This will list both valid and expired entries.
func (b *backend) pathWhitelistIdentitiesList(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
identities, err := req.Storage.List("whitelist/identity/")
if err != nil {
return nil, err
}
return logical.ListResponse(identities), nil
}
// Fetch an item from the whitelist given an instance ID.
func whitelistIdentityEntry(s logical.Storage, instanceID string) (*whitelistIdentity, error) {
entry, err := s.Get("whitelist/identity/" + instanceID)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result whitelistIdentity
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
// Stores an instance ID and the information required to validate further login/renewal attempts from
// the same instance ID.
func setWhitelistIdentityEntry(s logical.Storage, instanceID string, identity *whitelistIdentity) error {
entry, err := logical.StorageEntryJSON("whitelist/identity/"+instanceID, identity)
if err != nil {
return err
}
if err := s.Put(entry); err != nil {
return err
}
return nil
}
// pathIdentityWhitelistDelete is used to delete an entry from the identity whitelist given an instance ID.
func (b *backend) pathIdentityWhitelistDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
instanceID := data.Get("instance_id").(string)
if instanceID == "" {
return logical.ErrorResponse("missing instance_id"), nil
}
return nil, req.Storage.Delete("whitelist/identity/" + instanceID)
}
// pathIdentityWhitelistRead is used to view an entry in the identity whitelist given an instance ID.
func (b *backend) pathIdentityWhitelistRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
instanceID := data.Get("instance_id").(string)
if instanceID == "" {
return logical.ErrorResponse("missing instance_id"), nil
}
entry, err := whitelistIdentityEntry(req.Storage, instanceID)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(entry).Map(),
}, nil
}
// Struct to represent each item in the identity whitelist.
type whitelistIdentity struct {
Role string `json:"role" structs:"role" mapstructure:"role"`
ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"`
CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"`
DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"`
ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"`
LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"`
}
const pathIdentityWhitelistSyn = `
Read or delete entries in the identity whitelist.
`
const pathIdentityWhitelistDesc = `
Each login from an EC2 instance creates/updates an entry in the identity whitelist.
Entries in this list can be viewed or deleted using this endpoint.
By default, a cron task will periodically look for expired entries in the whitelist
and deletes them. The duration to periodically run this, is one hour by default.
However, this can be configured using the 'config/tidy/identities' endpoint. This tidy
action can be triggered via the API as well, using the 'tidy/identities' endpoint.
`
const pathListIdentityWhitelistHelpSyn = `
Lists the items present in the identity whitelist.
`
const pathListIdentityWhitelistHelpDesc = `
The entries in the identity whitelist is keyed off of the EC2 instance IDs.
This endpoint lists all the entries present in the identity whitelist, both
expired and un-expired entries. Use 'tidy/identities' endpoint to clean-up
the whitelist of identities.
`

View file

@ -0,0 +1,567 @@
package aws
import (
"encoding/json"
"encoding/pem"
"fmt"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/fullsailor/pkcs7"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: "login$",
Fields: map[string]*framework.FieldSchema{
"role": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Name of the role against which the login is being attempted.
If 'role' is not specified, then the login endpoint looks for a role
bearing the name of the AMI ID of the EC2 instance that is trying to login.
If a matching role is not found, login fails.`,
},
"pkcs7": &framework.FieldSchema{
Type: framework.TypeString,
Description: "PKCS7 signature of the identity document.",
},
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The nonce created by a client of this backend. When 'disallow_reauthentication'
option is enabled on either the role or the role tag, then nonce parameter is
optional. It is a required parameter otherwise.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLoginUpdate,
},
HelpSynopsis: pathLoginSyn,
HelpDescription: pathLoginDesc,
}
}
// validateInstance queries the status of the EC2 instance using AWS EC2 API and
// checks if the instance is running and is healthy.
func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.DescribeInstancesOutput, error) {
// Create an EC2 client to pull the instance information
ec2Client, err := b.clientEC2(s, region)
if err != nil {
return nil, err
}
status, err := ec2Client.DescribeInstances(&ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
&ec2.Filter{
Name: aws.String("instance-id"),
Values: []*string{
aws.String(instanceID),
},
},
},
})
if err != nil {
return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", instanceID, err)
}
if len(status.Reservations) == 0 {
return nil, fmt.Errorf("no reservations found in instance description")
}
if len(status.Reservations[0].Instances) == 0 {
return nil, fmt.Errorf("no instance details found in reservations")
}
if *status.Reservations[0].Instances[0].InstanceId != instanceID {
return nil, fmt.Errorf("expected instance ID not matching the instance ID in the instance description")
}
if status.Reservations[0].Instances[0].State == nil {
return nil, fmt.Errorf("instance state in instance description is nil")
}
if *status.Reservations[0].Instances[0].State.Code != 16 ||
*status.Reservations[0].Instances[0].State.Name != "running" {
return nil, fmt.Errorf("instance is not in 'running' state")
}
return status, nil
}
// validateMetadata matches the given client nonce and pending time with the one cached
// in the identity whitelist during the previous login. But, if reauthentication is
// disabled, login attempt is failed immediately.
func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, roleEntry *awsRoleEntry) error {
// If reauthentication is disabled, doesn't matter what other metadata is provided,
// authentication will not succeed.
if storedIdentity.DisallowReauthentication {
return fmt.Errorf("reauthentication is disabled")
}
givenPendingTime, err := time.Parse(time.RFC3339, pendingTime)
if err != nil {
return err
}
storedPendingTime, err := time.Parse(time.RFC3339, storedIdentity.PendingTime)
if err != nil {
return err
}
// When the presented client nonce does not match the cached entry, it is
// either that a rogue client is trying to login or that a valid client
// suffered a migration. The migration is detected via pendingTime in the
// instance metadata, which sadly is only updated when an instance is
// stopped and started but *not* when the instance is rebooted. If reboot
// survivability is needed, either instrumentation to delete the instance
// ID from the whitelist is necessary, or the client must durably store
// the nonce.
//
// If the `allow_instance_migration` property of the registered role is
// enabled, then the client nonce mismatch is ignored, as long as the
// pending time in the presented instance identity document is newer than
// the cached pending time. The new pendingTime is stored and used for
// future checks.
//
// This is a weak criterion and hence the `allow_instance_migration` option
// should be used with caution.
if clientNonce != storedIdentity.ClientNonce {
if !roleEntry.AllowInstanceMigration {
return fmt.Errorf("client nonce mismatch")
}
if roleEntry.AllowInstanceMigration && !givenPendingTime.After(storedPendingTime) {
return fmt.Errorf("client nonce mismatch and instance meta-data incorrect")
}
}
// Ensure that the 'pendingTime' on the given identity document is not before the
// 'pendingTime' that was used for previous login. This disallows old metadata documents
// from being used to perform login.
if givenPendingTime.Before(storedPendingTime) {
return fmt.Errorf("instance meta-data is older than the one used for previous login")
}
return nil
}
// Verifies the correctness of the authenticated attributes present in the PKCS#7
// signature. After verification, extracts the instance identity document from the
// signature, parses it and returns it.
func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) {
// Insert the header and footer for the signature to be able to pem decode it.
pkcs7B64 = fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", pkcs7B64)
// Decode the PEM encoded signature.
pkcs7BER, pkcs7Rest := pem.Decode([]byte(pkcs7B64))
if len(pkcs7Rest) != 0 {
return nil, fmt.Errorf("failed to decode the PEM encoded PKCS#7 signature")
}
// Parse the signature from asn1 format into a struct.
pkcs7Data, err := pkcs7.Parse(pkcs7BER.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse the BER encoded PKCS#7 signature: %s\n", err)
}
// Get the public certificates that are used to verify the signature.
// This returns a slice of certificates containing the default certificate
// and all the registered certificates via 'config/certificate/<cert_name>' endpoint
publicCerts, err := b.awsPublicCertificates(s)
if err != nil {
return nil, err
}
if publicCerts == nil || len(publicCerts) == 0 {
return nil, fmt.Errorf("certificates to verify the signature are not found")
}
// Before calling Verify() on the PKCS#7 struct, set the certificates to be used
// to verify the contents in the signer information.
pkcs7Data.Certificates = publicCerts
// Verify extracts the authenticated attributes in the PKCS#7 signature, and verifies
// the authenticity of the content using 'dsa.PublicKey' embedded in the public certificate.
if pkcs7Data.Verify() != nil {
return nil, fmt.Errorf("failed to verify the signature")
}
// Check if the signature has content inside of it.
if len(pkcs7Data.Content) == 0 {
return nil, fmt.Errorf("instance identity document could not be found in the signature")
}
var identityDoc identityDocument
err = json.Unmarshal(pkcs7Data.Content, &identityDoc)
if err != nil {
return nil, err
}
return &identityDoc, nil
}
// pathLoginUpdate is used to create a Vault token by the EC2 instances
// by providing the pkcs7 signature of the instance identity document
// and a client created nonce. Client nonce is optional if 'disallow_reauthentication'
// option is enabled on the registered role.
func (b *backend) pathLoginUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
pkcs7B64 := data.Get("pkcs7").(string)
if pkcs7B64 == "" {
return logical.ErrorResponse("missing pkcs7"), nil
}
// Verify the signature of the identity document.
identityDoc, err := b.parseIdentityDocument(req.Storage, pkcs7B64)
if err != nil {
return nil, err
}
if identityDoc == nil {
return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil
}
roleName := data.Get("role").(string)
// If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for.
if roleName == "" {
roleName = identityDoc.AmiID
}
// Validate the instance ID by making a call to AWS EC2 DescribeInstances API
// and fetching the instance description. Validation succeeds only if the
// instance is in 'running' state.
instanceDesc, err := b.validateInstance(req.Storage, identityDoc.InstanceID, identityDoc.Region)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil
}
// Get the entry for the role used by the instance.
roleEntry, err := b.awsRole(req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse("role entry not found"), nil
}
// Get the entry from the identity whitelist, if there is one.
storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDoc.InstanceID)
if err != nil {
return nil, err
}
clientNonce := data.Get("nonce").(string)
// This is NOT a first login attempt from the client.
if storedIdentity != nil {
// Check if the client nonce match the cached nonce and if the pending time
// of the identity document is not before the pending time of the document
// with which previous login was made. If 'allow_instance_migration' is
// enabled on the registered role, client nonce requirement is relaxed.
if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, roleEntry); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
}
// Load the current values for max TTL and policies from the role entry,
// before checking for overriding max TTL in the role tag. The shortest
// max TTL is used to cap the token TTL; the longest max TTL is used to
// make the whitelist entry as long as possible as it controls for replay
// attacks.
shortestMaxTTL := b.System().MaxLeaseTTL()
longestMaxTTL := b.System().MaxLeaseTTL()
if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL {
shortestMaxTTL = roleEntry.MaxTTL
}
if roleEntry.MaxTTL > longestMaxTTL {
longestMaxTTL = roleEntry.MaxTTL
}
policies := roleEntry.Policies
rTagMaxTTL := time.Duration(0)
// Read this value from the role entry; however, once it's been set, do not
// allow it to be flipped back. This prevents a role with this set to false
// to be overridden by a role tag, then have the role tag swapped and have
// this go back to false.
disallowReauthentication := roleEntry.DisallowReauthentication
if storedIdentity != nil {
if !disallowReauthentication && storedIdentity.DisallowReauthentication {
disallowReauthentication = true
}
}
if roleEntry.RoleTag != "" {
// Role tag is enabled on the role.
// Overwrite the policies with the ones returned from processing the role tag.
resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, roleName, roleEntry, instanceDesc)
if err != nil {
return nil, err
}
if resp == nil {
return logical.ErrorResponse("failed to fetch and verify the role tag"), nil
}
// If there are no policies on the role tag, policies on the role are inherited.
// If policies on role tag are set, by this point, it is verified that it is a subset of the
// policies on the role. So, apply only those.
if len(resp.Policies) != 0 {
policies = resp.Policies
}
// If roleEntry had disallowReauthentication set to 'true', do not reset it
// to 'false' based on role tag having it not set. But, if role tag had it set,
// be sure to override the value.
if !disallowReauthentication {
disallowReauthentication = resp.DisallowReauthentication
}
// Cache the value of role tag's max_ttl value.
rTagMaxTTL = resp.MaxTTL
// Scope the shortestMaxTTL to the value set on the role tag.
if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < shortestMaxTTL {
shortestMaxTTL = resp.MaxTTL
}
if resp.MaxTTL > longestMaxTTL {
longestMaxTTL = resp.MaxTTL
}
}
// Save the login attempt in the identity whitelist.
currentTime := time.Now().UTC()
if storedIdentity == nil {
// Role, ClientNonce and CreationTime of the identity entry,
// once set, should never change.
storedIdentity = &whitelistIdentity{
Role: roleName,
ClientNonce: clientNonce,
CreationTime: currentTime,
}
}
// DisallowReauthentication, PendingTime, LastUpdatedTime and ExpirationTime may change.
storedIdentity.LastUpdatedTime = currentTime
storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL)
storedIdentity.PendingTime = identityDoc.PendingTime
storedIdentity.DisallowReauthentication = disallowReauthentication
// Performing the clientNonce empty check after determining the DisallowReauthentication
// option. This is to make clientNonce optional when DisallowReauthentication is set.
if clientNonce == "" && !storedIdentity.DisallowReauthentication {
return logical.ErrorResponse("missing nonce"), nil
}
// Limit the nonce to a reasonable length.
if len(clientNonce) > 128 && !storedIdentity.DisallowReauthentication {
return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil
}
if err = setWhitelistIdentityEntry(req.Storage, identityDoc.InstanceID, storedIdentity); err != nil {
return nil, err
}
resp := &logical.Response{
Auth: &logical.Auth{
Policies: policies,
Metadata: map[string]string{
"instance_id": identityDoc.InstanceID,
"region": identityDoc.Region,
"role_tag_max_ttl": rTagMaxTTL.String(),
"role": roleName,
"ami_id": identityDoc.AmiID,
},
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: b.System().DefaultLeaseTTL(),
},
},
}
// Cap the TTL value.
if shortestMaxTTL < resp.Auth.TTL {
resp.Auth.TTL = shortestMaxTTL
}
return resp, nil
}
// handleRoleTagLogin is used to fetch the role tag of the instance and verifies it to be correct.
// Then the policies for the login request will be set off of the role tag, if certain creteria satisfies.
func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, roleName string, roleEntry *awsRoleEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) {
if identityDoc == nil {
return nil, fmt.Errorf("nil identityDoc")
}
if roleEntry == nil {
return nil, fmt.Errorf("nil roleEntry")
}
if instanceDesc == nil {
return nil, fmt.Errorf("nil instanceDesc")
}
// Input validation on instanceDesc is not performed here considering
// that it would have been done in validateInstance method.
tags := instanceDesc.Reservations[0].Instances[0].Tags
if tags == nil || len(tags) == 0 {
return nil, fmt.Errorf("missing tag with key %s on the instance", roleEntry.RoleTag)
}
// Iterate through the tags attached on the instance and look for
// a tag with its 'key' matching the expected role tag value.
rTagValue := ""
for _, tagItem := range tags {
if tagItem.Key != nil && *tagItem.Key == roleEntry.RoleTag {
rTagValue = *tagItem.Value
break
}
}
// If 'role_tag' is enabled on the role, and if a corresponding tag is not found
// to be attached to the instance, fail.
if rTagValue == "" {
return nil, fmt.Errorf("missing tag with key %s on the instance", roleEntry.RoleTag)
}
// Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC.
rTag, err := b.parseAndVerifyRoleTagValue(s, rTagValue)
if err != nil {
return nil, err
}
// Check if the role name with which this login is being made is same
// as the role name embedded in the tag.
if rTag.Role != roleName {
return nil, fmt.Errorf("role on the tag is not matching the role supplied")
}
// If instance_id was set on the role tag, check if the same instance is attempting to login.
if rTag.InstanceID != "" && rTag.InstanceID != identityDoc.InstanceID {
return nil, fmt.Errorf("role tag is being used by an unauthorized instance.")
}
// Check if the role tag is blacklisted.
blacklistEntry, err := b.blacklistRoleTagEntry(s, rTagValue)
if err != nil {
return nil, err
}
if blacklistEntry != nil {
return nil, fmt.Errorf("role tag is blacklisted")
}
// Ensure that the policies on the RoleTag is a subset of policies on the role
if !strutil.StrListSubset(roleEntry.Policies, rTag.Policies) {
return nil, fmt.Errorf("policies on the role tag must be subset of policies on the role")
}
return &roleTagLoginResponse{
Policies: rTag.Policies,
MaxTTL: rTag.MaxTTL,
DisallowReauthentication: rTag.DisallowReauthentication,
}, nil
}
// pathLoginRenew is used to renew an authenticated token.
func (b *backend) pathLoginRenew(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
instanceID := req.Auth.Metadata["instance_id"]
if instanceID == "" {
return nil, fmt.Errorf("unable to fetch instance ID from metadata during renewal")
}
region := req.Auth.Metadata["region"]
if region == "" {
return nil, fmt.Errorf("unable to fetch region from metadata during renewal")
}
// Cross check that the instance is still in 'running' state
_, err := b.validateInstance(req.Storage, instanceID, region)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil
}
storedIdentity, err := whitelistIdentityEntry(req.Storage, instanceID)
if err != nil {
return nil, err
}
// Ensure that role entry is not deleted.
roleEntry, err := b.awsRole(req.Storage, storedIdentity.Role)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse("role entry not found"), nil
}
// If the login was made using the role tag, then max_ttl from tag
// is cached in internal data during login and used here to cap the
// max_ttl of renewal.
rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"])
if err != nil {
return nil, err
}
// Re-evaluate the maxTTL bounds.
shortestMaxTTL := b.System().MaxLeaseTTL()
longestMaxTTL := b.System().MaxLeaseTTL()
if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL {
shortestMaxTTL = roleEntry.MaxTTL
}
if roleEntry.MaxTTL > longestMaxTTL {
longestMaxTTL = roleEntry.MaxTTL
}
if rTagMaxTTL > time.Duration(0) && rTagMaxTTL < shortestMaxTTL {
shortestMaxTTL = rTagMaxTTL
}
if rTagMaxTTL > longestMaxTTL {
longestMaxTTL = rTagMaxTTL
}
// Only LastUpdatedTime and ExpirationTime change and all other fields remain the same.
currentTime := time.Now().UTC()
storedIdentity.LastUpdatedTime = currentTime
storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL)
if err = setWhitelistIdentityEntry(req.Storage, instanceID, storedIdentity); err != nil {
return nil, err
}
return framework.LeaseExtend(req.Auth.TTL, shortestMaxTTL, b.System())(req, data)
}
// Struct to represent items of interest from the EC2 instance identity document.
type identityDocument struct {
Tags map[string]interface{} `json:"tags,omitempty" structs:"tags" mapstructure:"tags"`
InstanceID string `json:"instanceId,omitempty" structs:"instanceId" mapstructure:"instanceId"`
AmiID string `json:"imageId,omitempty" structs:"imageId" mapstructure:"imageId"`
Region string `json:"region,omitempty" structs:"region" mapstructure:"region"`
PendingTime string `json:"pendingTime,omitempty" structs:"pendingTime" mapstructure:"pendingTime"`
}
type roleTagLoginResponse struct {
Policies []string `json:"policies" structs:"policies" mapstructure:"policies"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"`
DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
}
const pathLoginSyn = `
Authenticates an EC2 instance with Vault.
`
const pathLoginDesc = `
An EC2 instance is authenticated using the PKCS#7 signature of the instance identity
document and a client created nonce. This nonce should be unique and should be used by
the instance for all future logins, unless 'disallow_reauthenitcation' option on the
registered role is enabled, in which case client nonce is optional.
First login attempt, creates a whitelist entry in Vault associating the instance to the nonce
provided. All future logins will succeed only if the client nonce matches the nonce in the
whitelisted entry.
By default, a cron task will periodically look for expired entries in the whitelist
and deletes them. The duration to periodically run this, is one hour by default.
However, this can be configured using the 'config/tidy/identities' endpoint. This tidy
action can be triggered via the API as well, using the 'tidy/identities' endpoint.
`

View file

@ -0,0 +1,335 @@
package aws
import (
"fmt"
"strings"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathRole(b *backend) *framework.Path {
return &framework.Path{
Pattern: "role/" + framework.GenericNameRegex("role"),
Fields: map[string]*framework.FieldSchema{
"role": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role.",
},
"bound_ami_id": &framework.FieldSchema{
Type: framework.TypeString,
Description: `If set, defines a constraint on the EC2 instances that they should be
using the AMI ID specified by this parameter.`,
},
"role_tag": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "If set, enables the role tags for this role. The value set for this field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using 'role/<role>/tag' endpoint. Defaults to an empty string, meaning that role tags are disabled.",
},
"max_ttl": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 0,
Description: "The maximum allowed lifetime of tokens issued using this role.",
},
"policies": &framework.FieldSchema{
Type: framework.TypeString,
Default: "default",
Description: "Policies to be set on tokens issued using this role.",
},
"allow_instance_migration": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution.",
},
"disallow_reauthentication": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws/identity-whitelist/<instance_id>' endpoint.",
},
},
ExistenceCheck: b.pathRoleExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathRoleCreateUpdate,
logical.UpdateOperation: b.pathRoleCreateUpdate,
logical.ReadOperation: b.pathRoleRead,
logical.DeleteOperation: b.pathRoleDelete,
},
HelpSynopsis: pathRoleSyn,
HelpDescription: pathRoleDesc,
}
}
func pathListRole(b *backend) *framework.Path {
return &framework.Path{
Pattern: "role/?",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoleList,
},
HelpSynopsis: pathListRolesHelpSyn,
HelpDescription: pathListRolesHelpDesc,
}
}
func pathListRoles(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roles/?",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoleList,
},
HelpSynopsis: pathListRolesHelpSyn,
HelpDescription: pathListRolesHelpDesc,
}
}
// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
func (b *backend) pathRoleExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
entry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role").(string)))
if err != nil {
return false, err
}
return entry != nil, nil
}
// awsRole is used to get the information registered for the given AMI ID.
func (b *backend) awsRole(s logical.Storage, role string) (*awsRoleEntry, error) {
b.roleMutex.RLock()
defer b.roleMutex.RUnlock()
return b.awsRoleInternal(s, role)
}
func (b *backend) awsRoleInternal(s logical.Storage, role string) (*awsRoleEntry, error) {
entry, err := s.Get("role/" + strings.ToLower(role))
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result awsRoleEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
// pathRoleDelete is used to delete the information registered for a given AMI ID.
func (b *backend) pathRoleDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
if roleName == "" {
return logical.ErrorResponse("missing role"), nil
}
b.roleMutex.Lock()
defer b.roleMutex.Unlock()
return nil, req.Storage.Delete("role/" + strings.ToLower(roleName))
}
// pathRoleList is used to list all the AMI IDs registered with Vault.
func (b *backend) pathRoleList(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.roleMutex.RLock()
defer b.roleMutex.RUnlock()
roles, err := req.Storage.List("role/")
if err != nil {
return nil, err
}
return logical.ListResponse(roles), nil
}
// pathRoleRead is used to view the information registered for a given AMI ID.
func (b *backend) pathRoleRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleEntry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role").(string)))
if err != nil {
return nil, err
}
if roleEntry == nil {
return nil, nil
}
// Prepare the map of all the entries in the roleEntry.
respData := structs.New(roleEntry).Map()
// HMAC key belonging to the role should NOT be exported.
delete(respData, "hmac_key")
// Display the max_ttl in seconds.
respData["max_ttl"] = roleEntry.MaxTTL / time.Second
return &logical.Response{
Data: respData,
}, nil
}
// pathRoleCreateUpdate is used to associate Vault policies to a given AMI ID.
func (b *backend) pathRoleCreateUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := strings.ToLower(data.Get("role").(string))
if roleName == "" {
return logical.ErrorResponse("missing role"), nil
}
b.roleMutex.Lock()
defer b.roleMutex.Unlock()
roleEntry, err := b.awsRoleInternal(req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
roleEntry = &awsRoleEntry{}
}
// Set the bound parameters only if they are supplied.
// There are no default values for bound parameters.
boundAmiIDStr, ok := data.GetOk("bound_ami_id")
if ok {
roleEntry.BoundAmiID = boundAmiIDStr.(string)
}
// At least one bound parameter should be set. Currently, only
// 'bound_ami_id' is supported. Check if that is set.
if roleEntry.BoundAmiID == "" {
return logical.ErrorResponse("role is not bounded to any resource; set bound_ami_id"), nil
}
policiesStr, ok := data.GetOk("policies")
if ok {
roleEntry.Policies = policyutil.ParsePolicies(policiesStr.(string))
} else if req.Operation == logical.CreateOperation {
roleEntry.Policies = []string{"default"}
}
disallowReauthenticationBool, ok := data.GetOk("disallow_reauthentication")
if ok {
roleEntry.DisallowReauthentication = disallowReauthenticationBool.(bool)
} else if req.Operation == logical.CreateOperation {
roleEntry.DisallowReauthentication = data.Get("disallow_reauthentication").(bool)
}
allowInstanceMigrationBool, ok := data.GetOk("allow_instance_migration")
if ok {
roleEntry.AllowInstanceMigration = allowInstanceMigrationBool.(bool)
} else if req.Operation == logical.CreateOperation {
roleEntry.AllowInstanceMigration = data.Get("allow_instance_migration").(bool)
}
var resp logical.Response
maxTTLInt, ok := data.GetOk("max_ttl")
if ok {
maxTTL := time.Duration(maxTTLInt.(int)) * time.Second
systemMaxTTL := b.System().MaxLeaseTTL()
if maxTTL > systemMaxTTL {
resp.AddWarning(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds; TTL will be capped at login time", maxTTL/time.Second, systemMaxTTL/time.Second))
}
if maxTTL < time.Duration(0) {
return logical.ErrorResponse("max_ttl cannot be negative"), nil
}
roleEntry.MaxTTL = maxTTL
} else if req.Operation == logical.CreateOperation {
roleEntry.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second
}
roleTagStr, ok := data.GetOk("role_tag")
if ok {
roleEntry.RoleTag = roleTagStr.(string)
// There is a limit of 127 characters on the tag key for AWS EC2 instances.
// Complying to that requirement, do not allow the value of 'key' to be more than that.
if len(roleEntry.RoleTag) > 127 {
return logical.ErrorResponse("length of role tag exceeds the EC2 key limit of 127 characters"), nil
}
} else if req.Operation == logical.CreateOperation {
roleEntry.RoleTag = data.Get("role_tag").(string)
}
if roleEntry.HMACKey == "" {
roleEntry.HMACKey, err = uuid.GenerateUUID()
if err != nil {
return nil, fmt.Errorf("failed to generate role HMAC key: %v", err)
}
}
entry, err := logical.StorageEntryJSON("role/"+roleName, roleEntry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
if len(resp.Warnings()) == 0 {
return nil, nil
}
return &resp, nil
}
// Struct to hold the information associated with an AMI ID in Vault.
type awsRoleEntry struct {
BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"`
RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"`
AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"`
Policies []string `json:"policies" structs:"policies" mapstructure:"policies"`
DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"`
}
const pathRoleSyn = `
Create a role and associate policies to it.
`
const pathRoleDesc = `
A precondition for login is that a role should be created in the backend.
The login endpoint takes in the role name against which the instance
should be validated. After authenticating the instance, the authorization
for the instance to access Vault's resources is determined by the policies
that are associated to the role though this endpoint.
When the instances require only a subset of policies on the role, then
'role_tag' option on the role can be enabled to create a role tag via the
endpoint 'role/<role>/tag'. This tag then needs to be applied on the
instance before it attempts a login. The policies on the tag should be a
subset of policies that are associated to the role. In order to enable
login using tags, 'role_tag' option should be set while creating a role.
Also, a 'max_ttl' can be configured in this endpoint that determines the maximum
duration for which a login can be renewed. Note that the 'max_ttl' has an upper
limit of the 'max_ttl' value on the backend's mount.
`
const pathListRolesHelpSyn = `
Lists all the roles that are registered with Vault.
`
const pathListRolesHelpDesc = `
Roles will be listed by their respective role names.
`

View file

@ -0,0 +1,432 @@
package aws
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const roleTagVersion = "v1"
func pathRoleTag(b *backend) *framework.Path {
return &framework.Path{
Pattern: "role/" + framework.GenericNameRegex("role") + "/tag$",
Fields: map[string]*framework.FieldSchema{
"role": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role.",
},
"instance_id": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Instance ID for which this tag is intended for.
If set, the created tag can only be used by the instance with the given ID.`,
},
"policies": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Policies to be associated with the tag. If set, must be a subset of the role's policies. If set, but set to an empty value, only the 'default' policy will be given to issued tokens.",
},
"max_ttl": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 0,
Description: "If set, specifies the maximum allowed token lifetime.",
},
"allow_instance_migration": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution.",
},
"disallow_reauthentication": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using the 'auth/aws/identity-whitelist/<instance_id>' endpoint.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRoleTagUpdate,
},
HelpSynopsis: pathRoleTagSyn,
HelpDescription: pathRoleTagDesc,
}
}
// pathRoleTagUpdate is used to create an EC2 instance tag which will
// identify the Vault resources that the instance will be authorized for.
func (b *backend) pathRoleTagUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := strings.ToLower(data.Get("role").(string))
if roleName == "" {
return logical.ErrorResponse("missing role"), nil
}
// Fetch the role entry
roleEntry, err := b.awsRole(req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry not found for role %s", roleName)), nil
}
// If RoleTag is empty, disallow creation of tag.
if roleEntry.RoleTag == "" {
return logical.ErrorResponse("tag creation is not enabled for this role"), nil
}
// There should be a HMAC key present in the role entry
if roleEntry.HMACKey == "" {
// Not being able to find the HMACKey is an internal error
return nil, fmt.Errorf("failed to find the HMAC key")
}
resp := &logical.Response{}
// Instance ID is an optional field.
instanceID := strings.ToLower(data.Get("instance_id").(string))
// If no policies field was not supplied, then the tag should inherit all the policies
// on the role. But, it was provided, but set to empty explicitly, only "default" policy
// should be inherited. So, by leaving the policies var unset to anything when it is not
// supplied, we ensure that it inherits all the policies on the role.
var policies []string
policiesStr, ok := data.GetOk("policies")
if ok {
policies = policyutil.ParsePolicies(policiesStr.(string))
}
if !strutil.StrListSubset(roleEntry.Policies, policies) {
resp.AddWarning("Policies on the tag are not a subset of the policies set on the role. Login will not be allowed with this tag unless the role policies are updated.")
}
// This is an optional field.
disallowReauthentication := data.Get("disallow_reauthentication").(bool)
// This is an optional field.
allowInstanceMigration := data.Get("allow_instance_migration").(bool)
if allowInstanceMigration && !roleEntry.AllowInstanceMigration {
resp.AddWarning("Role does not allow instance migration. Login will not be allowed with this tag unless the role value is updated.")
}
// max_ttl for the role tag should be less than the max_ttl set on the role.
maxTTL := time.Duration(data.Get("max_ttl").(int)) * time.Second
// max_ttl on the tag should not be greater than the system view's max_ttl value.
if maxTTL > b.System().MaxLeaseTTL() {
resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the mount maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second))
}
// If max_ttl is set for the role, check the bounds for tag's max_ttl value using that.
if roleEntry.MaxTTL != time.Duration(0) && maxTTL > roleEntry.MaxTTL {
resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the role maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, roleEntry.MaxTTL/time.Second))
}
if maxTTL < time.Duration(0) {
return logical.ErrorResponse("max_ttl cannot be negative"), nil
}
// Create a random nonce.
nonce, err := createRoleTagNonce()
if err != nil {
return nil, err
}
// Create a role tag out of all the information provided.
rTagValue, err := createRoleTagValue(&roleTag{
Version: roleTagVersion,
Role: roleName,
Nonce: nonce,
Policies: policies,
MaxTTL: maxTTL,
InstanceID: instanceID,
DisallowReauthentication: disallowReauthentication,
AllowInstanceMigration: allowInstanceMigration,
}, roleEntry)
if err != nil {
return nil, err
}
// Return the key to be used for the tag and the value to be used for that tag key.
// This key value pair should be set on the EC2 instance.
resp.Data = map[string]interface{}{
"tag_key": roleEntry.RoleTag,
"tag_value": rTagValue,
}
return resp, nil
}
// createRoleTagValue prepares the plaintext version of the role tag,
// and appends a HMAC of the plaintext value to it, before returning.
func createRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (string, error) {
if rTag == nil {
return "", fmt.Errorf("nil role tag")
}
if roleEntry == nil {
return "", fmt.Errorf("nil role entry")
}
// Attach version, nonce, policies and maxTTL to the role tag value.
rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag)
if err != nil {
return "", err
}
// Attach HMAC to tag's plaintext and return.
return appendHMAC(rTagPlaintext, roleEntry)
}
// Takes in the plaintext part of the role tag, creates a HMAC of it and returns
// a role tag value containing both the plaintext part and the HMAC part.
func appendHMAC(rTagPlaintext string, roleEntry *awsRoleEntry) (string, error) {
if rTagPlaintext == "" {
return "", fmt.Errorf("empty role tag plaintext string")
}
if roleEntry == nil {
return "", fmt.Errorf("nil role entry")
}
// Create the HMAC of the value
hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext)
if err != nil {
return "", err
}
// attach the HMAC to the value
rTagValue := fmt.Sprintf("%s:%s", rTagPlaintext, hmacB64)
// This limit of 255 is enforced on the EC2 instance. Hence complying to that here.
if len(rTagValue) > 255 {
return "", fmt.Errorf("role tag 'value' exceeding the limit of 255 characters")
}
return rTagValue, nil
}
// verifyRoleTagValue rebuilds the role tag's plaintext part, computes the HMAC
// from it using the role specific HMAC key and compares it with the received HMAC.
func verifyRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (bool, error) {
if rTag == nil {
return false, fmt.Errorf("nil role tag")
}
if roleEntry == nil {
return false, fmt.Errorf("nil role entry")
}
// Fetch the plaintext part of role tag
rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag)
if err != nil {
return false, err
}
// Compute the HMAC of the plaintext
hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext)
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare([]byte(rTag.HMAC), []byte(hmacB64)) == 1, nil
}
// prepareRoleTagPlaintextValue builds the role tag value without the HMAC in it.
func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) {
if rTag == nil {
return "", fmt.Errorf("nil role tag")
}
if rTag.Version == "" {
return "", fmt.Errorf("missing version")
}
if rTag.Nonce == "" {
return "", fmt.Errorf("missing nonce")
}
if rTag.Role == "" {
return "", fmt.Errorf("missing role")
}
// Attach Version, Nonce, Role, DisallowReauthentication and AllowInstanceMigration
// fields to the role tag.
value := fmt.Sprintf("%s:%s:r=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.Role, strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration))
// Attach the policies only if they are specified.
if len(rTag.Policies) != 0 {
value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ","))
}
// Attach instance_id if set.
if rTag.InstanceID != "" {
value = fmt.Sprintf("%s:i=%s", value, rTag.InstanceID)
}
// Attach max_ttl if it is provided.
if int(rTag.MaxTTL.Seconds()) > 0 {
value = fmt.Sprintf("%s:t=%d", value, int(rTag.MaxTTL.Seconds()))
}
return value, nil
}
// Parses the tag from string form into a struct form. This method
// also verifies the correctness of the parsed role tag.
func (b *backend) parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) {
tagItems := strings.Split(tag, ":")
// Tag must contain version, nonce, policies and HMAC
if len(tagItems) < 4 {
return nil, fmt.Errorf("invalid tag")
}
rTag := &roleTag{}
// Cache the HMAC value. The last item in the collection.
rTag.HMAC = tagItems[len(tagItems)-1]
// Remove the HMAC from the list.
tagItems = tagItems[:len(tagItems)-1]
// Version will be the first element.
rTag.Version = tagItems[0]
if rTag.Version != roleTagVersion {
return nil, fmt.Errorf("invalid role tag version")
}
// Nonce will be the second element.
rTag.Nonce = tagItems[1]
// Delete the version and nonce from the list.
tagItems = tagItems[2:]
for _, tagItem := range tagItems {
var err error
switch {
case strings.Contains(tagItem, "i="):
rTag.InstanceID = strings.TrimPrefix(tagItem, "i=")
case strings.Contains(tagItem, "r="):
rTag.Role = strings.TrimPrefix(tagItem, "r=")
case strings.Contains(tagItem, "p="):
rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",")
case strings.Contains(tagItem, "d="):
rTag.DisallowReauthentication, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "d="))
if err != nil {
return nil, err
}
case strings.Contains(tagItem, "m="):
rTag.AllowInstanceMigration, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "m="))
if err != nil {
return nil, err
}
case strings.Contains(tagItem, "t="):
rTag.MaxTTL, err = time.ParseDuration(fmt.Sprintf("%ss", strings.TrimPrefix(tagItem, "t=")))
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unrecognized item %s in tag", tagItem)
}
}
if rTag.Role == "" {
return nil, fmt.Errorf("missing role name")
}
roleEntry, err := b.awsRole(s, rTag.Role)
if err != nil {
return nil, err
}
if roleEntry == nil {
return nil, fmt.Errorf("entry not found for %s", rTag.Role)
}
// Create a HMAC of the plaintext value of role tag and compare it with the given value.
verified, err := verifyRoleTagValue(rTag, roleEntry)
if err != nil {
return nil, err
}
if !verified {
return nil, fmt.Errorf("role tag signature verification failed")
}
return rTag, nil
}
// Creates base64 encoded HMAC using a per-role key.
func createRoleTagHMACBase64(key, value string) (string, error) {
if key == "" {
return "", fmt.Errorf("invalid HMAC key")
}
hm := hmac.New(sha256.New, []byte(key))
hm.Write([]byte(value))
// base64 encode the hmac bytes.
return base64.StdEncoding.EncodeToString(hm.Sum(nil)), nil
}
// Creates a base64 encoded random nonce.
func createRoleTagNonce() (string, error) {
if uuidBytes, err := uuid.GenerateRandomBytes(8); err != nil {
return "", err
} else {
return base64.StdEncoding.EncodeToString(uuidBytes), nil
}
}
// Struct roleTag represents a role tag in a struc form.
type roleTag struct {
Version string `json:"version" structs:"version" mapstructure:"version"`
InstanceID string `json:"instance_id" structs:"instance_id" mapstructure:"instance_id"`
Nonce string `json:"nonce" structs:"nonce" mapstructure:"nonce"`
Policies []string `json:"policies" structs:"policies" mapstructure:"policies"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"`
Role string `json:"role" structs:"role" mapstructure:"role"`
HMAC string `json:"hmac" structs:"hmac" mapstructure:"hmac"`
DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"`
}
func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool {
return rTag1 != nil &&
rTag2 != nil &&
rTag1.Version == rTag2.Version &&
rTag1.Nonce == rTag2.Nonce &&
policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) &&
rTag1.MaxTTL == rTag2.MaxTTL &&
rTag1.Role == rTag2.Role &&
rTag1.HMAC == rTag2.HMAC &&
rTag1.InstanceID == rTag2.InstanceID &&
rTag1.DisallowReauthentication == rTag2.DisallowReauthentication &&
rTag1.AllowInstanceMigration == rTag2.AllowInstanceMigration
}
const pathRoleTagSyn = `
Create a tag on a role in order to be able to further restrict the capabilities of a role.
`
const pathRoleTagDesc = `
If there are needs to apply only a subset of role's capabilities to any specific
instance, create a role tag using this endpoint and attach the tag on the instance
before performing login.
To be able to create a role tag, the 'role_tag' option on the role should be
enabled via the endpoint 'role/<role>'. Also, the policies to be associated
with the tag should be a subset of the policies associated with the registered role.
This endpoint will return both the 'key' and the 'value' of the tag to be set
on the EC2 instance.
`

View file

@ -0,0 +1,255 @@
package aws
import (
"encoding/base64"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathRoletagBlacklist(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roletag-blacklist/(?P<role_tag>.*)",
Fields: map[string]*framework.FieldSchema{
"role_tag": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Role tag to be blacklisted. The tag can be supplied as-is. In order
to avoid any encoding problems, it can be base64 encoded.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRoletagBlacklistUpdate,
logical.ReadOperation: b.pathRoletagBlacklistRead,
logical.DeleteOperation: b.pathRoletagBlacklistDelete,
},
HelpSynopsis: pathRoletagBlacklistSyn,
HelpDescription: pathRoletagBlacklistDesc,
}
}
// Path to list all the blacklisted tags.
func pathListRoletagBlacklist(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roletag-blacklist/?",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathRoletagBlacklistsList,
},
HelpSynopsis: pathListRoletagBlacklistHelpSyn,
HelpDescription: pathListRoletagBlacklistHelpDesc,
}
}
// Lists all the blacklisted role tags.
func (b *backend) pathRoletagBlacklistsList(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.blacklistMutex.RLock()
defer b.blacklistMutex.RUnlock()
tags, err := req.Storage.List("blacklist/roletag/")
if err != nil {
return nil, err
}
// Tags are base64 encoded before indexing to avoid problems
// with the path separators being present in the tag.
// Reverse it before returning the list response.
for i, keyB64 := range tags {
if key, err := base64.StdEncoding.DecodeString(keyB64); err != nil {
return nil, err
} else {
// Overwrite the result with the decoded string.
tags[i] = string(key)
}
}
return logical.ListResponse(tags), nil
}
// Fetch an entry from the role tag blacklist for a given tag.
// This method takes a role tag in its original form and not a base64 encoded form.
func (b *backend) blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) {
b.blacklistMutex.RLock()
defer b.blacklistMutex.RUnlock()
return b.blacklistRoleTagEntryInternal(s, tag)
}
func (b *backend) blacklistRoleTagEntryInternal(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) {
entry, err := s.Get("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag)))
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result roleTagBlacklistEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
// Deletes an entry from the role tag blacklist for a given tag.
func (b *backend) pathRoletagBlacklistDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.blacklistMutex.Lock()
defer b.blacklistMutex.Unlock()
tag := data.Get("role_tag").(string)
if tag == "" {
return logical.ErrorResponse("missing role_tag"), nil
}
return nil, req.Storage.Delete("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag)))
}
// If the given role tag is blacklisted, returns the details of the blacklist entry.
// Returns 'nil' otherwise.
func (b *backend) pathRoletagBlacklistRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
tag := data.Get("role_tag").(string)
if tag == "" {
return logical.ErrorResponse("missing role_tag"), nil
}
entry, err := b.blacklistRoleTagEntry(req.Storage, tag)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(entry).Map(),
}, nil
}
// pathRoletagBlacklistUpdate is used to blacklist a given role tag.
// Before a role tag is blacklisted, the correctness of the plaintext part
// in the role tag is verified using the associated HMAC.
func (b *backend) pathRoletagBlacklistUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// The role_tag value provided, optionally can be base64 encoded.
tagInput := data.Get("role_tag").(string)
if tagInput == "" {
return logical.ErrorResponse("missing role_tag"), nil
}
tag := ""
// Try to base64 decode the value.
tagBytes, err := base64.StdEncoding.DecodeString(tagInput)
if err != nil {
// If the decoding failed, use the value as-is.
tag = tagInput
} else {
// If the decoding succeeded, use the decoded value.
tag = string(tagBytes)
}
// Parse and verify the role tag from string form to a struct form and verify it.
rTag, err := b.parseAndVerifyRoleTagValue(req.Storage, tag)
if err != nil {
return nil, err
}
if rTag == nil {
return logical.ErrorResponse("failed to verify the role tag and parse it"), nil
}
// Get the entry for the role mentioned in the role tag.
roleEntry, err := b.awsRole(req.Storage, rTag.Role)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse("role entry not found"), nil
}
b.blacklistMutex.Lock()
defer b.blacklistMutex.Unlock()
// Check if the role tag is already blacklisted. If yes, update it.
blEntry, err := b.blacklistRoleTagEntryInternal(req.Storage, tag)
if err != nil {
return nil, err
}
if blEntry == nil {
blEntry = &roleTagBlacklistEntry{}
}
currentTime := time.Now().UTC()
// Check if this is a creation of blacklist entry.
if blEntry.CreationTime.IsZero() {
// Set the creation time for the blacklist entry.
// This should not be updated after setting it once.
// If blacklist operation is invoked more than once, only update the expiration time.
blEntry.CreationTime = currentTime
}
// Decide the expiration time based on the max_ttl values. Since this is
// restricting access, use the greatest duration, not the least.
maxDur := rTag.MaxTTL
if roleEntry.MaxTTL > maxDur {
maxDur = roleEntry.MaxTTL
}
if b.System().MaxLeaseTTL() > maxDur {
maxDur = b.System().MaxLeaseTTL()
}
blEntry.ExpirationTime = currentTime.Add(maxDur)
entry, err := logical.StorageEntryJSON("blacklist/roletag/"+base64.StdEncoding.EncodeToString([]byte(tag)), blEntry)
if err != nil {
return nil, err
}
// Store the blacklist entry.
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
type roleTagBlacklistEntry struct {
CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"`
ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"`
}
const pathRoletagBlacklistSyn = `
Blacklist a previously created role tag.
`
const pathRoletagBlacklistDesc = `
Blacklist a role tag so that it cannot be used by any EC2 instance to perform further
logins. This can be used if the role tag is suspected or believed to be possessed by
an unintended party.
By default, a cron task will periodically look for expired entries in the blacklist
and deletes them. The duration to periodically run this, is one hour by default.
However, this can be configured using the 'config/tidy/roletags' endpoint. This tidy
action can be triggered via the API as well, using the 'tidy/roletags' endpoint.
Also note that delete operation is supported on this endpoint to remove specific
entries from the blacklist.
`
const pathListRoletagBlacklistHelpSyn = `
Lists the blacklisted role tags.
`
const pathListRoletagBlacklistHelpDesc = `
Lists all the entries present in the blacklist. This will show both the valid
entries and the expired entries in the blacklist. Use 'tidy/roletags' endpoint
to clean-up the blacklist of role tags based on expiration time.
`

View file

@ -0,0 +1,96 @@
package aws
import (
"fmt"
"sync/atomic"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathTidyIdentityWhitelist(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy/identity-whitelist$",
Fields: map[string]*framework.FieldSchema{
"safety_buffer": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 259200,
Description: `The amount of extra time that must have passed beyond the identity's
expiration, before it is removed from the backend storage.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathTidyIdentityWhitelistUpdate,
},
HelpSynopsis: pathTidyIdentityWhitelistSyn,
HelpDescription: pathTidyIdentityWhitelistDesc,
}
}
// tidyWhitelistIdentity is used to delete entries in the whitelist that are expired.
func (b *backend) tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error {
grabbed := atomic.CompareAndSwapUint32(&b.tidyWhitelistCASGuard, 0, 1)
if grabbed {
defer atomic.StoreUint32(&b.tidyWhitelistCASGuard, 0)
} else {
return fmt.Errorf("identity whitelist tidy operation already running")
}
bufferDuration := time.Duration(safety_buffer) * time.Second
identities, err := s.List("whitelist/identity/")
if err != nil {
return err
}
for _, instanceID := range identities {
identityEntry, err := s.Get("whitelist/identity/" + instanceID)
if err != nil {
return fmt.Errorf("error fetching identity of instanceID %s: %s", instanceID, err)
}
if identityEntry == nil {
return fmt.Errorf("identity entry for instanceID %s is nil", instanceID)
}
if identityEntry.Value == nil || len(identityEntry.Value) == 0 {
return fmt.Errorf("found identity entry for instanceID %s but actual identity is empty", instanceID)
}
var result whitelistIdentity
if err := identityEntry.DecodeJSON(&result); err != nil {
return err
}
if time.Now().UTC().After(result.ExpirationTime.Add(bufferDuration)) {
if err := s.Delete("whitelist/identity" + instanceID); err != nil {
return fmt.Errorf("error deleting identity of instanceID %s from storage: %s", instanceID, err)
}
}
}
return nil
}
// pathTidyIdentityWhitelistUpdate is used to delete entries in the whitelist that are expired.
func (b *backend) pathTidyIdentityWhitelistUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return nil, b.tidyWhitelistIdentity(req.Storage, data.Get("safety_buffer").(int))
}
const pathTidyIdentityWhitelistSyn = `
Clean-up the whitelist instance identity entries.
`
const pathTidyIdentityWhitelistDesc = `
When an instance identity is whitelisted, the expiration time of the whitelist
entry is set based on the maximum 'max_ttl' value set on: the role, the role tag
and the backend's mount.
When this endpoint is invoked, all the entries that are expired will be deleted.
A 'safety_buffer' (duration in seconds) can be provided, to ensure deletion of
only those entries that are expired before 'safety_buffer' seconds.
`

View file

@ -0,0 +1,95 @@
package aws
import (
"fmt"
"sync/atomic"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathTidyRoletagBlacklist(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy/roletag-blacklist$",
Fields: map[string]*framework.FieldSchema{
"safety_buffer": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 259200, // 72h
Description: `The amount of extra time that must have passed beyond the roletag
expiration, before it is removed from the backend storage.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathTidyRoletagBlacklistUpdate,
},
HelpSynopsis: pathTidyRoletagBlacklistSyn,
HelpDescription: pathTidyRoletagBlacklistDesc,
}
}
// tidyBlacklistRoleTag is used to clean-up the entries in the role tag blacklist.
func (b *backend) tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error {
grabbed := atomic.CompareAndSwapUint32(&b.tidyBlacklistCASGuard, 0, 1)
if grabbed {
defer atomic.StoreUint32(&b.tidyBlacklistCASGuard, 0)
} else {
return fmt.Errorf("roletag blacklist tidy operation already running")
}
bufferDuration := time.Duration(safety_buffer) * time.Second
tags, err := s.List("blacklist/roletag/")
if err != nil {
return err
}
for _, tag := range tags {
tagEntry, err := s.Get("blacklist/roletag/" + tag)
if err != nil {
return fmt.Errorf("error fetching tag %s: %s", tag, err)
}
if tagEntry == nil {
return fmt.Errorf("tag entry for tag %s is nil", tag)
}
if tagEntry.Value == nil || len(tagEntry.Value) == 0 {
return fmt.Errorf("found entry for tag %s but actual tag is empty", tag)
}
var result roleTagBlacklistEntry
if err := tagEntry.DecodeJSON(&result); err != nil {
return err
}
if time.Now().UTC().After(result.ExpirationTime.Add(bufferDuration)) {
if err := s.Delete("blacklist/roletag" + tag); err != nil {
return fmt.Errorf("error deleting tag %s from storage: %s", tag, err)
}
}
}
return nil
}
// pathTidyRoletagBlacklistUpdate is used to clean-up the entries in the role tag blacklist.
func (b *backend) pathTidyRoletagBlacklistUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return nil, b.tidyBlacklistRoleTag(req.Storage, data.Get("safety_buffer").(int))
}
const pathTidyRoletagBlacklistSyn = `
Clean-up the blacklist role tag entries.
`
const pathTidyRoletagBlacklistDesc = `
When a role tag is blacklisted, the expiration time of the blacklist entry is
set based on the maximum 'max_ttl' value set on: the role, the role tag and the
backend's mount.
When this endpoint is invoked, all the entries that are expired will be deleted.
A 'safety_buffer' (duration in seconds) can be provided, to ensure deletion of
only those entries that are expired before 'safety_buffer' seconds.
`

View file

@ -35,8 +35,8 @@ func Backend() *framework.Backend {
secretAccessKeys(&b),
},
Rollback: rollback,
RollbackMinAge: 5 * time.Minute,
WALRollback: walRollback,
WALRollbackMinAge: 5 * time.Minute,
}
return b.Backend

View file

@ -7,12 +7,12 @@ import (
"github.com/hashicorp/vault/logical/framework"
)
var rollbackMap = map[string]framework.RollbackFunc{
var walRollbackMap = map[string]framework.WALRollbackFunc{
"user": pathUserRollback,
}
func rollback(req *logical.Request, kind string, data interface{}) error {
f, ok := rollbackMap[kind]
func walRollback(req *logical.Request, kind string, data interface{}) error {
f, ok := walRollbackMap[kind]
if !ok {
return fmt.Errorf("unknown type to rollback")
}

View file

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/vault/version"
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
credAws "github.com/hashicorp/vault/builtin/credential/aws"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
@ -63,6 +64,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
},
CredentialBackends: map[string]logical.Factory{
"cert": credCert.Factory,
"aws": credAws.Factory,
"app-id": credAppId.Factory,
"github": credGitHub.Factory,
"userpass": credUserpass.Factory,

View file

@ -42,14 +42,24 @@ type Backend struct {
// and ease specifying callbacks for revocation, renewal, etc.
Secrets []*Secret
// Rollback is called when a WAL entry (see wal.go) has to be rolled
// PeriodicFunc is the callback, which if set, will be invoked when the
// periodic timer of RollbackManager ticks. This can be used by
// backends to do anything it wishes to do periodically.
//
// PeriodicFunc can be invoked to, say to periodically delete stale
// entries in backend's storage, while the backend is still being used.
// (Note the different of this action from what `Clean` does, which is
// invoked just before the backend is unmounted).
PeriodicFunc periodicFunc
// WALRollback is called when a WAL entry (see wal.go) has to be rolled
// back. It is called with the data from the entry.
//
// RollbackMinAge is the minimum age of a WAL entry before it is attempted
// WALRollbackMinAge is the minimum age of a WAL entry before it is attempted
// to be rolled back. This should be longer than the maximum time it takes
// to successfully create a secret.
Rollback RollbackFunc
RollbackMinAge time.Duration
WALRollback WALRollbackFunc
WALRollbackMinAge time.Duration
// Clean is called on unload to clean up e.g any existing connections
// to the backend, if required.
@ -66,11 +76,15 @@ type Backend struct {
pathsRe []*regexp.Regexp
}
// periodicFunc is the callback called when the RollbackManager's timer ticks.
// This can be utilized by the backends to do anything it wants.
type periodicFunc func(*logical.Request) error
// OperationFunc is the callback called for an operation on a path.
type OperationFunc func(*logical.Request, *FieldData) (*logical.Response, error)
// RollbackFunc is the callback for rollbacks.
type RollbackFunc func(*logical.Request, string, interface{}) error
// WALRollbackFunc is the callback for rollbacks.
type WALRollbackFunc func(*logical.Request, string, interface{}) error
// CleanupFunc is the callback for backend unload.
type CleanupFunc func()
@ -394,6 +408,19 @@ func (b *Backend) handleRevokeRenew(
}
}
// handleRollback invokes the PeriodicFunc set on the backend. It also does a WAL rollback operation.
func (b *Backend) handleRollback(
req *logical.Request) (*logical.Response, error) {
// Response is not expected from the periodic operation.
if b.PeriodicFunc != nil {
if err := b.PeriodicFunc(req); err != nil {
return nil, err
}
}
return b.handleWALRollback(req)
}
func (b *Backend) handleAuthRenew(req *logical.Request) (*logical.Response, error) {
if b.AuthRenew == nil {
return logical.ErrorResponse("this auth type doesn't support renew"), nil
@ -402,9 +429,9 @@ func (b *Backend) handleAuthRenew(req *logical.Request) (*logical.Response, erro
return b.AuthRenew(req, nil)
}
func (b *Backend) handleRollback(
func (b *Backend) handleWALRollback(
req *logical.Request) (*logical.Response, error) {
if b.Rollback == nil {
if b.WALRollback == nil {
return nil, logical.ErrUnsupportedOperation
}
@ -419,7 +446,7 @@ func (b *Backend) handleRollback(
// Calculate the minimum time that the WAL entries could be
// created in order to be rolled back.
age := b.RollbackMinAge
age := b.WALRollbackMinAge
if age == 0 {
age = 10 * time.Minute
}
@ -443,8 +470,8 @@ func (b *Backend) handleRollback(
continue
}
// Attempt a rollback
err = b.Rollback(req, entry.Kind, entry.Data)
// Attempt a WAL rollback
err = b.WALRollback(req, entry.Kind, entry.Data)
if err != nil {
err = fmt.Errorf(
"Error rolling back '%s' entry: %s", entry.Kind, err)

View file

@ -314,8 +314,8 @@ func TestBackendHandleRequest_rollback(t *testing.T) {
}
b := &Backend{
Rollback: callback,
RollbackMinAge: 1 * time.Millisecond,
WALRollback: callback,
WALRollbackMinAge: 1 * time.Millisecond,
}
storage := new(logical.InmemStorage)
@ -349,8 +349,8 @@ func TestBackendHandleRequest_rollbackMinAge(t *testing.T) {
}
b := &Backend{
Rollback: callback,
RollbackMinAge: 5 * time.Second,
WALRollback: callback,
WALRollbackMinAge: 5 * time.Second,
}
storage := new(logical.InmemStorage)

View file

@ -30,10 +30,10 @@ const (
type RollbackManager struct {
logger *log.Logger
// This gives the current mount table, plus a RWMutex that is
// locked for reading. It is up to the caller to RUnlock it
// when done with the mount table
mounts func() []*MountEntry
// This gives the current mount table of both logical and credential backends,
// plus a RWMutex that is locked for reading. It is up to the caller to RUnlock
// it when done with the mount table.
backends func() []*MountEntry
router *Router
period time.Duration
@ -55,10 +55,10 @@ type rollbackState struct {
}
// NewRollbackManager is used to create a new rollback manager
func NewRollbackManager(logger *log.Logger, mounts func() []*MountEntry, router *Router) *RollbackManager {
func NewRollbackManager(logger *log.Logger, backendsFunc func() []*MountEntry, router *Router) *RollbackManager {
r := &RollbackManager{
logger: logger,
mounts: mounts,
backends: backendsFunc,
router: router,
period: rollbackPeriod,
inflight: make(map[string]*rollbackState),
@ -109,9 +109,9 @@ func (m *RollbackManager) triggerRollbacks() {
m.inflightLock.Lock()
defer m.inflightLock.Unlock()
mounts := m.mounts()
backends := m.backends()
for _, e := range mounts {
for _, e := range backends {
if _, ok := m.inflight[e.Path]; !ok {
m.startRollback(e.Path)
}
@ -184,16 +184,24 @@ func (m *RollbackManager) Rollback(path string) error {
// startRollback is used to start the rollback manager after unsealing
func (c *Core) startRollback() error {
mountsFunc := func() []*MountEntry {
backendsFunc := func() []*MountEntry {
ret := []*MountEntry{}
c.mountsLock.RLock()
defer c.mountsLock.RUnlock()
for _, entry := range c.mounts.Entries {
ret = append(ret, entry)
}
c.authLock.RLock()
defer c.authLock.RUnlock()
for _, entry := range c.auth.Entries {
if !strings.HasPrefix(entry.Path, "auth/") {
entry.Path = "auth/" + entry.Path
}
ret = append(ret, entry)
}
return ret
}
c.rollback = NewRollbackManager(c.logger, mountsFunc, c.router)
c.rollback = NewRollbackManager(c.logger, backendsFunc, c.router)
c.rollback.Start()
return nil
}

24
vendor/github.com/fullsailor/pkcs7/.gitignore generated vendored Normal file
View file

@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

22
vendor/github.com/fullsailor/pkcs7/LICENSE generated vendored Normal file
View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Andrew Smith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
vendor/github.com/fullsailor/pkcs7/README.md generated vendored Normal file
View file

@ -0,0 +1,7 @@
# pkcs7
[![GoDoc](https://godoc.org/github.com/fullsailor/pkcs7?status.svg)](https://godoc.org/github.com/fullsailor/pkcs7)
pkcs7 implements parsing and creating signed and enveloped messages.
- Documentation on [GoDoc](http://godoc.org/github.com/fullsailor/pkcs7)

228
vendor/github.com/fullsailor/pkcs7/ber.go generated vendored Normal file
View file

@ -0,0 +1,228 @@
package pkcs7
import (
"bytes"
"errors"
)
var encodeIndent = 0
type asn1Object interface {
EncodeTo(writer *bytes.Buffer) error
}
type asn1Structured struct {
tagBytes []byte
content []asn1Object
}
func (s asn1Structured) EncodeTo(out *bytes.Buffer) error {
//fmt.Printf("%s--> tag: % X\n", strings.Repeat("| ", encodeIndent), s.tagBytes)
encodeIndent++
inner := new(bytes.Buffer)
for _, obj := range s.content {
err := obj.EncodeTo(inner)
if err != nil {
return err
}
}
encodeIndent--
out.Write(s.tagBytes)
encodeLength(out, inner.Len())
out.Write(inner.Bytes())
return nil
}
type asn1Primitive struct {
tagBytes []byte
length int
content []byte
}
func (p asn1Primitive) EncodeTo(out *bytes.Buffer) error {
_, err := out.Write(p.tagBytes)
if err != nil {
return err
}
if err = encodeLength(out, p.length); err != nil {
return err
}
//fmt.Printf("%s--> tag: % X length: %d\n", strings.Repeat("| ", encodeIndent), p.tagBytes, p.length)
//fmt.Printf("%s--> content length: %d\n", strings.Repeat("| ", encodeIndent), len(p.content))
out.Write(p.content)
return nil
}
func ber2der(ber []byte) ([]byte, error) {
if len(ber) == 0 {
return nil, errors.New("ber2der: input ber is empty")
}
//fmt.Printf("--> ber2der: Transcoding %d bytes\n", len(ber))
out := new(bytes.Buffer)
obj, _, err := readObject(ber, 0)
if err != nil {
return nil, err
}
obj.EncodeTo(out)
// if offset < len(ber) {
// return nil, fmt.Errorf("ber2der: Content longer than expected. Got %d, expected %d", offset, len(ber))
//}
return out.Bytes(), nil
}
// encodes lengths that are longer than 127 into string of bytes
func marshalLongLength(out *bytes.Buffer, i int) (err error) {
n := lengthLength(i)
for ; n > 0; n-- {
err = out.WriteByte(byte(i >> uint((n-1)*8)))
if err != nil {
return
}
}
return nil
}
// computes the byte length of an encoded length value
func lengthLength(i int) (numBytes int) {
numBytes = 1
for i > 255 {
numBytes++
i >>= 8
}
return
}
// encodes the length in DER format
// If the length fits in 7 bits, the value is encoded directly.
//
// Otherwise, the number of bytes to encode the length is first determined.
// This number is likely to be 4 or less for a 32bit length. This number is
// added to 0x80. The length is encoded in big endian encoding follow after
//
// Examples:
// length | byte 1 | bytes n
// 0 | 0x00 | -
// 120 | 0x78 | -
// 200 | 0x81 | 0xC8
// 500 | 0x82 | 0x01 0xF4
//
func encodeLength(out *bytes.Buffer, length int) (err error) {
if length >= 128 {
l := lengthLength(length)
err = out.WriteByte(0x80 | byte(l))
if err != nil {
return
}
err = marshalLongLength(out, length)
if err != nil {
return
}
} else {
err = out.WriteByte(byte(length))
if err != nil {
return
}
}
return
}
func readObject(ber []byte, offset int) (asn1Object, int, error) {
//fmt.Printf("\n====> Starting readObject at offset: %d\n\n", offset)
tagStart := offset
b := ber[offset]
offset++
tag := b & 0x1F // last 5 bits
if tag == 0x1F {
tag = 0
for ber[offset] >= 0x80 {
tag = tag*128 + ber[offset] - 0x80
offset++
}
tag = tag*128 + ber[offset] - 0x80
offset++
}
tagEnd := offset
kind := b & 0x20
/*
if kind == 0 {
fmt.Print("--> Primitive\n")
} else {
fmt.Print("--> Constructed\n")
}
*/
// read length
var length int
l := ber[offset]
offset++
hack := 0
if l > 0x80 {
numberOfBytes := (int)(l & 0x7F)
if numberOfBytes > 4 { // int is only guaranteed to be 32bit
return nil, 0, errors.New("ber2der: BER tag length too long")
}
if numberOfBytes == 4 && (int)(ber[offset]) > 0x7F {
return nil, 0, errors.New("ber2der: BER tag length is negative")
}
if 0x0 == (int)(ber[offset]) {
return nil, 0, errors.New("ber2der: BER tag length has leading zero")
}
//fmt.Printf("--> (compute length) indicator byte: %x\n", l)
//fmt.Printf("--> (compute length) length bytes: % X\n", ber[offset:offset+numberOfBytes])
for i := 0; i < numberOfBytes; i++ {
length = length*256 + (int)(ber[offset])
offset++
}
} else if l == 0x80 {
// find length by searching content
markerIndex := bytes.LastIndex(ber[offset:], []byte{0x0, 0x0})
if markerIndex == -1 {
return nil, 0, errors.New("ber2der: Invalid BER format")
}
length = markerIndex
hack = 2
//fmt.Printf("--> (compute length) marker found at offset: %d\n", markerIndex+offset)
} else {
length = (int)(l)
}
//fmt.Printf("--> length : %d\n", length)
contentEnd := offset + length
if contentEnd > len(ber) {
return nil, 0, errors.New("ber2der: BER tag length is more than available data")
}
//fmt.Printf("--> content start : %d\n", offset)
//fmt.Printf("--> content end : %d\n", contentEnd)
//fmt.Printf("--> content : % X\n", ber[offset:contentEnd])
var obj asn1Object
if kind == 0 {
obj = asn1Primitive{
tagBytes: ber[tagStart:tagEnd],
length: length,
content: ber[offset:contentEnd],
}
} else {
var subObjects []asn1Object
for offset < contentEnd {
var subObj asn1Object
var err error
subObj, offset, err = readObject(ber[:contentEnd], offset)
if err != nil {
return nil, 0, err
}
subObjects = append(subObjects, subObj)
}
obj = asn1Structured{
tagBytes: ber[tagStart:tagEnd],
content: subObjects,
}
}
return obj, contentEnd + hack, nil
}

61
vendor/github.com/fullsailor/pkcs7/ber_test.go generated vendored Normal file
View file

@ -0,0 +1,61 @@
package pkcs7
import (
"bytes"
"encoding/asn1"
"strings"
"testing"
)
func TestBer2Der(t *testing.T) {
// indefinite length fixture
ber := []byte{0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00}
expected := []byte{0x30, 0x03, 0x02, 0x01, 0x01}
der, err := ber2der(ber)
if err != nil {
t.Fatalf("ber2der failed with error: %v", err)
}
if bytes.Compare(der, expected) != 0 {
t.Errorf("ber2der result did not match.\n\tExpected: % X\n\tActual: % X", expected, der)
}
if der2, err := ber2der(der); err != nil {
t.Errorf("ber2der on DER bytes failed with error: %v", err)
} else {
if !bytes.Equal(der, der2) {
t.Error("ber2der is not idempotent")
}
}
var thing struct {
Number int
}
rest, err := asn1.Unmarshal(der, &thing)
if err != nil {
t.Errorf("Cannot parse resulting DER because: %v", err)
} else if len(rest) > 0 {
t.Errorf("Resulting DER has trailing data: % X", rest)
}
}
func TestBer2Der_Negatives(t *testing.T) {
fixtures := []struct {
Input []byte
ErrorContains string
}{
{[]byte{0x30, 0x85}, "length too long"},
{[]byte{0x30, 0x84, 0x80, 0x0, 0x0, 0x0}, "length is negative"},
{[]byte{0x30, 0x82, 0x0, 0x1}, "length has leading zero"},
{[]byte{0x30, 0x80, 0x1, 0x2}, "Invalid BER format"},
{[]byte{0x30, 0x03, 0x01, 0x02}, "length is more than available data"},
}
for _, fixture := range fixtures {
_, err := ber2der(fixture.Input)
if err == nil {
t.Errorf("No error thrown. Expected: %s", fixture.ErrorContains)
}
if !strings.Contains(err.Error(), fixture.ErrorContains) {
t.Errorf("Unexpected error thrown.\n\tExpected: /%s/\n\tActual: %s", fixture.ErrorContains, err.Error())
}
}
}

776
vendor/github.com/fullsailor/pkcs7/pkcs7.go generated vendored Normal file
View file

@ -0,0 +1,776 @@
// Package pkcs7 implements parsing and generation of some PKCS#7 structures.
package pkcs7
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/des"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"sort"
"time"
_ "crypto/sha1" // for crypto.SHA1
)
// PKCS7 Represents a PKCS7 structure
type PKCS7 struct {
Content []byte
Certificates []*x509.Certificate
CRLs []pkix.CertificateList
Signers []signerInfo
raw interface{}
}
type contentInfo struct {
ContentType asn1.ObjectIdentifier
Content asn1.RawValue `asn1:"explicit,optional,tag:0"`
}
// ErrUnsupportedContentType is returned when a PKCS7 content is not supported.
// Currently only Data (1.2.840.113549.1.7.1), Signed Data (1.2.840.113549.1.7.2),
// and Enveloped Data are supported (1.2.840.113549.1.7.3)
var ErrUnsupportedContentType = errors.New("pkcs7: cannot parse data: unimplemented content type")
type unsignedData []byte
var (
oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
oidEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 3}
oidSignedAndEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 4}
oidDigestedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 5}
oidEncryptedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 6}
oidAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
oidAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
oidAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
)
type signedData struct {
Version int `asn1:"default:1"`
DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
ContentInfo contentInfo
Certificates rawCertificates `asn1:"optional,tag:0"`
CRLs []pkix.CertificateList `asn1:"optional,tag:1"`
SignerInfos []signerInfo `asn1:"set"`
}
type rawCertificates struct {
Raw asn1.RawContent
}
type envelopedData struct {
Version int
RecipientInfos []recipientInfo `asn1:"set"`
EncryptedContentInfo encryptedContentInfo
}
type recipientInfo struct {
Version int
IssuerAndSerialNumber issuerAndSerial
KeyEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedKey []byte
}
type encryptedContentInfo struct {
ContentType asn1.ObjectIdentifier
ContentEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedContent asn1.RawValue `asn1:"tag:0,optional,explicit"`
}
type attribute struct {
Type asn1.ObjectIdentifier
Value asn1.RawValue `asn1:"set"`
}
type issuerAndSerial struct {
IssuerName asn1.RawValue
SerialNumber *big.Int
}
// MessageDigestMismatchError is returned when the signer data digest does not
// match the computed digest for the contained content
type MessageDigestMismatchError struct {
ExpectedDigest []byte
ActualDigest []byte
}
func (err *MessageDigestMismatchError) Error() string {
return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest)
}
type signerInfo struct {
Version int `asn1:"default:1"`
IssuerAndSerialNumber issuerAndSerial
DigestAlgorithm pkix.AlgorithmIdentifier
AuthenticatedAttributes []attribute `asn1:"optional,tag:0"`
DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedDigest []byte
UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"`
}
// Parse decodes a DER encoded PKCS7 package
func Parse(data []byte) (p7 *PKCS7, err error) {
if len(data) == 0 {
return nil, errors.New("pkcs7: input data is empty")
}
var info contentInfo
der, err := ber2der(data)
if err != nil {
return nil, err
}
rest, err := asn1.Unmarshal(der, &info)
if len(rest) > 0 {
err = asn1.SyntaxError{Msg: "trailing data"}
return
}
if err != nil {
return
}
// fmt.Printf("--> Content Type: %s", info.ContentType)
switch {
case info.ContentType.Equal(oidSignedData):
return parseSignedData(info.Content.Bytes)
case info.ContentType.Equal(oidEnvelopedData):
return parseEnvelopedData(info.Content.Bytes)
}
return nil, ErrUnsupportedContentType
}
func parseSignedData(data []byte) (*PKCS7, error) {
var sd signedData
asn1.Unmarshal(data, &sd)
certs, err := sd.Certificates.Parse()
if err != nil {
return nil, err
}
// fmt.Printf("--> Signed Data Version %d\n", sd.Version)
var compound asn1.RawValue
var content unsignedData
// The Content.Bytes maybe empty on PKI responses.
if len(sd.ContentInfo.Content.Bytes) > 0 {
if _, err := asn1.Unmarshal(sd.ContentInfo.Content.Bytes, &compound); err != nil {
return nil, err
}
}
// Compound octet string
if compound.IsCompound {
if _, err = asn1.Unmarshal(compound.Bytes, &content); err != nil {
return nil, err
}
} else {
// assuming this is tag 04
content = compound.Bytes
}
return &PKCS7{
Content: content,
Certificates: certs,
CRLs: sd.CRLs,
Signers: sd.SignerInfos,
raw: sd}, nil
}
func (raw rawCertificates) Parse() ([]*x509.Certificate, error) {
if len(raw.Raw) == 0 {
return nil, nil
}
var val asn1.RawValue
if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil {
return nil, err
}
return x509.ParseCertificates(val.Bytes)
}
func parseEnvelopedData(data []byte) (*PKCS7, error) {
var ed envelopedData
if _, err := asn1.Unmarshal(data, &ed); err != nil {
return nil, err
}
return &PKCS7{
raw: ed,
}, nil
}
// Verify checks the signatures of a PKCS7 object
// WARNING: Verify does not check signing time or verify certificate chains at
// this time.
func (p7 *PKCS7) Verify() (err error) {
if len(p7.Signers) == 0 {
return errors.New("pkcs7: Message has no signers")
}
for _, signer := range p7.Signers {
if err := verifySignature(p7, signer); err != nil {
return err
}
}
return nil
}
func verifySignature(p7 *PKCS7, signer signerInfo) error {
if len(signer.AuthenticatedAttributes) > 0 {
// TODO(fullsailor): First check the content type match
var digest []byte
err := unmarshalAttribute(signer.AuthenticatedAttributes, oidAttributeMessageDigest, &digest)
if err != nil {
return err
}
hash, err := getHashForOID(signer.DigestAlgorithm.Algorithm)
if err != nil {
return err
}
h := hash.New()
h.Write(p7.Content)
computed := h.Sum(nil)
if !hmac.Equal(digest, computed) {
return &MessageDigestMismatchError{
ExpectedDigest: digest,
ActualDigest: computed,
}
}
}
cert := getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber)
if cert == nil {
return errors.New("pkcs7: No certificate for signer")
}
// TODO(fullsailor): Optionally verify certificate chain
// TODO(fullsailor): Optionally verify signingTime against certificate NotAfter/NotBefore
encodedAttributes, err := marshalAttributes(signer.AuthenticatedAttributes)
if err != nil {
return err
}
algo := x509.SHA1WithRSA
return cert.CheckSignature(algo, encodedAttributes, signer.EncryptedDigest)
}
func marshalAttributes(attrs []attribute) ([]byte, error) {
encodedAttributes, err := asn1.Marshal(struct {
A []attribute `asn1:"set"`
}{A: attrs})
if err != nil {
return nil, err
}
// Remove the leading sequence octets
var raw asn1.RawValue
asn1.Unmarshal(encodedAttributes, &raw)
return raw.Bytes, nil
}
var (
oidDigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
oidEncryptionAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
)
func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndSerial) *x509.Certificate {
for _, cert := range certs {
if isCertMatchForIssuerAndSerial(cert, ias) {
return cert
}
}
return nil
}
func getHashForOID(oid asn1.ObjectIdentifier) (crypto.Hash, error) {
switch {
case oid.Equal(oidDigestAlgorithmSHA1):
return crypto.SHA1, nil
}
return crypto.Hash(0), ErrUnsupportedAlgorithm
}
// GetOnlySigner returns an x509.Certificate for the first signer of the signed
// data payload. If there are more or less than one signer, nil is returned
func (p7 *PKCS7) GetOnlySigner() *x509.Certificate {
if len(p7.Signers) != 1 {
return nil
}
signer := p7.Signers[0]
return getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber)
}
// ErrUnsupportedAlgorithm tells you when our quick dev assumptions have failed
var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3 and AES-256-CBC supported")
// ErrNotEncryptedContent is returned when attempting to Decrypt data that is not encrypted data
var ErrNotEncryptedContent = errors.New("pkcs7: content data is a decryptable data type")
// Decrypt decrypts encrypted content info for recipient cert and private key
func (p7 *PKCS7) Decrypt(cert *x509.Certificate, pk crypto.PrivateKey) ([]byte, error) {
data, ok := p7.raw.(envelopedData)
if !ok {
return nil, ErrNotEncryptedContent
}
recipient := selectRecipientForCertificate(data.RecipientInfos, cert)
if recipient.EncryptedKey == nil {
return nil, errors.New("pkcs7: no enveloped recipient for provided certificate")
}
if priv := pk.(*rsa.PrivateKey); priv != nil {
var contentKey []byte
contentKey, err := rsa.DecryptPKCS1v15(rand.Reader, priv, recipient.EncryptedKey)
if err != nil {
return nil, err
}
return data.EncryptedContentInfo.decrypt(contentKey)
}
fmt.Printf("Unsupported Private Key: %v\n", pk)
return nil, ErrUnsupportedAlgorithm
}
var oidEncryptionAlgorithmDESCBC = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 7}
var oidEncryptionAlgorithmDESEDE3CBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 3, 7}
var oidEncryptionAlgorithmAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42}
func (eci encryptedContentInfo) decrypt(key []byte) ([]byte, error) {
alg := eci.ContentEncryptionAlgorithm.Algorithm
if !alg.Equal(oidEncryptionAlgorithmDESCBC) && !alg.Equal(oidEncryptionAlgorithmDESEDE3CBC) && !alg.Equal(oidEncryptionAlgorithmAES256CBC) {
fmt.Printf("Unsupported Content Encryption Algorithm: %s\n", alg)
return nil, ErrUnsupportedAlgorithm
}
// EncryptedContent can either be constructed of multple OCTET STRINGs
// or _be_ a tagged OCTET STRING
var cyphertext []byte
if eci.EncryptedContent.IsCompound {
// Complex case to concat all of the children OCTET STRINGs
var buf bytes.Buffer
cypherbytes := eci.EncryptedContent.Bytes
for {
var part []byte
cypherbytes, _ = asn1.Unmarshal(cypherbytes, &part)
buf.Write(part)
if cypherbytes == nil {
break
}
}
cyphertext = buf.Bytes()
} else {
// Simple case, the bytes _are_ the cyphertext
cyphertext = eci.EncryptedContent.Bytes
}
var block cipher.Block
var err error
switch {
case alg.Equal(oidEncryptionAlgorithmDESCBC):
block, err = des.NewCipher(key)
case alg.Equal(oidEncryptionAlgorithmDESEDE3CBC):
block, err = des.NewTripleDESCipher(key)
case alg.Equal(oidEncryptionAlgorithmAES256CBC):
block, err = aes.NewCipher(key)
}
if err != nil {
return nil, err
}
iv := eci.ContentEncryptionAlgorithm.Parameters.Bytes
if len(iv) != block.BlockSize() {
return nil, errors.New("pkcs7: encryption algorithm parameters are malformed")
}
mode := cipher.NewCBCDecrypter(block, iv)
plaintext := make([]byte, len(cyphertext))
mode.CryptBlocks(plaintext, cyphertext)
if plaintext, err = unpad(plaintext, mode.BlockSize()); err != nil {
return nil, err
}
return plaintext, nil
}
func selectRecipientForCertificate(recipients []recipientInfo, cert *x509.Certificate) recipientInfo {
for _, recp := range recipients {
if isCertMatchForIssuerAndSerial(cert, recp.IssuerAndSerialNumber) {
return recp
}
}
return recipientInfo{}
}
func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool {
return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Compare(cert.RawIssuer, ias.IssuerName.FullBytes) == 0
}
func pad(data []byte, blocklen int) ([]byte, error) {
if blocklen < 1 {
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
}
padlen := blocklen - (len(data) % blocklen)
if padlen == 0 {
padlen = blocklen
}
pad := bytes.Repeat([]byte{byte(padlen)}, padlen)
return append(data, pad...), nil
}
func unpad(data []byte, blocklen int) ([]byte, error) {
if blocklen < 1 {
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
}
if len(data)%blocklen != 0 || len(data) == 0 {
return nil, fmt.Errorf("invalid data len %d", len(data))
}
// the last byte is the length of padding
padlen := int(data[len(data)-1])
// check padding integrity, all bytes should be the same
pad := data[len(data)-padlen:]
for _, padbyte := range pad {
if padbyte != byte(padlen) {
return nil, errors.New("invalid padding")
}
}
return data[:len(data)-padlen], nil
}
func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error {
for _, attr := range attrs {
if attr.Type.Equal(attributeType) {
_, err := asn1.Unmarshal(attr.Value.Bytes, out)
return err
}
}
return errors.New("pkcs7: attribute type not in attributes")
}
// UnmarshalSignedAttribute decodes a single attribute from the signer info
func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, out interface{}) error {
sd, ok := p7.raw.(signedData)
if !ok {
return errors.New("pkcs7: payload is not signedData content")
}
if len(sd.SignerInfos) < 1 {
return errors.New("pkcs7: payload has no signers")
}
attributes := sd.SignerInfos[0].AuthenticatedAttributes
return unmarshalAttribute(attributes, attributeType, out)
}
// SignedData is an opaque data structure for creating signed data payloads
type SignedData struct {
sd signedData
certs []*x509.Certificate
messageDigest []byte
}
// Attribute represents a key value pair attribute. Value must be marshalable byte
// `encoding/asn1`
type Attribute struct {
Type asn1.ObjectIdentifier
Value interface{}
}
// SignerInfoConfig are optional values to include when adding a signer
type SignerInfoConfig struct {
ExtraSignedAttributes []Attribute
}
// NewSignedData initializes a SignedData with content
func NewSignedData(data []byte) (*SignedData, error) {
content, err := asn1.Marshal(data)
if err != nil {
return nil, err
}
ci := contentInfo{
ContentType: oidData,
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true},
}
digAlg := pkix.AlgorithmIdentifier{
Algorithm: oidDigestAlgorithmSHA1,
}
h := crypto.SHA1.New()
h.Write(data)
md := h.Sum(nil)
sd := signedData{
ContentInfo: ci,
Version: 1,
DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{digAlg},
}
return &SignedData{sd: sd, messageDigest: md}, nil
}
type attributes struct {
types []asn1.ObjectIdentifier
values []interface{}
}
// Add adds the attribute, maintaining insertion order
func (attrs *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) {
attrs.types = append(attrs.types, attrType)
attrs.values = append(attrs.values, value)
}
type sortableAttribute struct {
SortKey []byte
Attribute attribute
}
type attributeSet []sortableAttribute
func (sa attributeSet) Len() int {
return len(sa)
}
func (sa attributeSet) Less(i, j int) bool {
return bytes.Compare(sa[i].SortKey, sa[j].SortKey) < 0
}
func (sa attributeSet) Swap(i, j int) {
sa[i], sa[j] = sa[j], sa[i]
}
func (sa attributeSet) Attributes() []attribute {
attrs := make([]attribute, len(sa))
for i, attr := range sa {
attrs[i] = attr.Attribute
}
return attrs
}
func (attrs *attributes) ForMarshaling() ([]attribute, error) {
sortables := make(attributeSet, len(attrs.types))
for i := range sortables {
attrType := attrs.types[i]
attrValue := attrs.values[i]
asn1Value, err := asn1.Marshal(attrValue)
if err != nil {
return nil, err
}
attr := attribute{
Type: attrType,
Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag
}
encoded, err := asn1.Marshal(attr)
if err != nil {
return nil, err
}
sortables[i] = sortableAttribute{
SortKey: encoded,
Attribute: attr,
}
}
sort.Sort(sortables)
return sortables.Attributes(), nil
}
// AddSigner signs attributes about the content and adds certificate to payload
func (sd *SignedData) AddSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error {
attrs := &attributes{}
attrs.Add(oidAttributeContentType, sd.sd.ContentInfo.ContentType)
attrs.Add(oidAttributeMessageDigest, sd.messageDigest)
attrs.Add(oidAttributeSigningTime, time.Now())
for _, attr := range config.ExtraSignedAttributes {
attrs.Add(attr.Type, attr.Value)
}
finalAttrs, err := attrs.ForMarshaling()
if err != nil {
return err
}
signature, err := signAttributes(finalAttrs, pkey, crypto.SHA1)
if err != nil {
return err
}
ias, err := cert2issuerAndSerial(cert)
if err != nil {
return err
}
signer := signerInfo{
AuthenticatedAttributes: finalAttrs,
DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidDigestAlgorithmSHA1},
DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidEncryptionAlgorithmRSA},
IssuerAndSerialNumber: ias,
EncryptedDigest: signature,
Version: 1,
}
// create signature of signed attributes
sd.certs = append(sd.certs, cert)
sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer)
return nil
}
// AddCertificate adds the certificate to the payload. Useful for parent certificates
func (sd *SignedData) AddCertificate(cert *x509.Certificate) {
sd.certs = append(sd.certs, cert)
}
// Finish marshals the content and its signers
func (sd *SignedData) Finish() ([]byte, error) {
sd.sd.Certificates = marshalCertificates(sd.certs)
inner, err := asn1.Marshal(sd.sd)
if err != nil {
return nil, err
}
outer := contentInfo{
ContentType: oidSignedData,
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true},
}
return asn1.Marshal(outer)
}
func cert2issuerAndSerial(cert *x509.Certificate) (issuerAndSerial, error) {
var ias issuerAndSerial
// The issuer RDNSequence has to match exactly the sequence in the certificate
// We cannot use cert.Issuer.ToRDNSequence() here since it mangles the sequence
ias.IssuerName = asn1.RawValue{FullBytes: cert.RawIssuer}
ias.SerialNumber = cert.SerialNumber
return ias, nil
}
// signs the DER encoded form of the attributes with the private key
func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) {
attrBytes, err := marshalAttributes(attrs)
if err != nil {
return nil, err
}
h := hash.New()
h.Write(attrBytes)
hashed := h.Sum(nil)
switch priv := pkey.(type) {
case *rsa.PrivateKey:
return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed)
}
return nil, ErrUnsupportedAlgorithm
}
// concats and wraps the certificates in the RawValue structure
func marshalCertificates(certs []*x509.Certificate) rawCertificates {
var buf bytes.Buffer
for _, cert := range certs {
buf.Write(cert.Raw)
}
// Even though, the tag & length are stripped out during marshalling the
// RawContent, we have to encode it into the RawContent. If its missing,
// then `asn1.Marshal()` will strip out the certificate wrapper instead.
var val = asn1.RawValue{Bytes: buf.Bytes(), Class: 2, Tag: 0, IsCompound: true}
b, _ := asn1.Marshal(val)
return rawCertificates{Raw: b}
}
// DegenerateCertificate creates a signed data structure containing only the
// provided certificate
func DegenerateCertificate(cert []byte) ([]byte, error) {
emptyContent := contentInfo{ContentType: oidData}
sd := signedData{
Version: 1,
ContentInfo: emptyContent,
Certificates: rawCertificates{Raw: cert},
CRLs: []pkix.CertificateList{},
}
content, err := asn1.Marshal(sd)
if err != nil {
return nil, err
}
signedContent := contentInfo{
ContentType: oidSignedData,
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true},
}
return asn1.Marshal(signedContent)
}
// Encrypt creates and returns an envelope data PKCS7 structure with encrypted
// recipient keys for each recipient public key
// TODO(fullsailor): Add support for encrypting content with other algorithms
func Encrypt(content []byte, recipients []*x509.Certificate) ([]byte, error) {
// Create DES key & CBC IV
key := make([]byte, 8)
iv := make([]byte, des.BlockSize)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
_, err = rand.Read(iv)
if err != nil {
return nil, err
}
// Encrypt padded content
block, err := des.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCEncrypter(block, iv)
plaintext, err := pad(content, mode.BlockSize())
cyphertext := make([]byte, len(plaintext))
mode.CryptBlocks(cyphertext, plaintext)
// Prepare ASN.1 Encrypted Content Info
eci := encryptedContentInfo{
ContentType: oidData,
ContentEncryptionAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oidEncryptionAlgorithmDESCBC,
Parameters: asn1.RawValue{Tag: 4, Bytes: iv},
},
EncryptedContent: marshalEncryptedContent(cyphertext),
}
// Prepare each recipient's encrypted cipher key
recipientInfos := make([]recipientInfo, len(recipients))
for i, recipient := range recipients {
encrypted, err := encryptKey(key, recipient)
if err != nil {
return nil, err
}
ias, err := cert2issuerAndSerial(recipient)
if err != nil {
return nil, err
}
info := recipientInfo{
Version: 0,
IssuerAndSerialNumber: ias,
KeyEncryptionAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oidEncryptionAlgorithmRSA,
},
EncryptedKey: encrypted,
}
recipientInfos[i] = info
}
// Prepare envelope content
envelope := envelopedData{
EncryptedContentInfo: eci,
Version: 0,
RecipientInfos: recipientInfos,
}
innerContent, err := asn1.Marshal(envelope)
if err != nil {
return nil, err
}
// Prepare outer payload structure
wrapper := contentInfo{
ContentType: oidEnvelopedData,
Content: asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: innerContent},
}
return asn1.Marshal(wrapper)
}
func marshalEncryptedContent(content []byte) asn1.RawValue {
asn1Content, _ := asn1.Marshal(content)
return asn1.RawValue{Tag: 0, Class: 2, Bytes: asn1Content, IsCompound: true}
}
func encryptKey(key []byte, recipient *x509.Certificate) ([]byte, error) {
if pub := recipient.PublicKey.(*rsa.PublicKey); pub != nil {
return rsa.EncryptPKCS1v15(rand.Reader, pub, key)
}
return nil, ErrUnsupportedAlgorithm
}

410
vendor/github.com/fullsailor/pkcs7/pkcs7_test.go generated vendored Normal file
View file

@ -0,0 +1,410 @@
package pkcs7
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"io"
"math/big"
"testing"
"time"
)
func TestVerify(t *testing.T) {
fixture := UnmarshalTestFixture(SignedTestFixture)
p7, err := Parse(fixture.Input)
if err != nil {
t.Errorf("Parse encountered unexpected error: %v", err)
}
if err := p7.Verify(); err != nil {
t.Errorf("Verify failed with error: %v", err)
}
expected := []byte("We the People")
if bytes.Compare(p7.Content, expected) != 0 {
t.Errorf("Signed content does not match.\n\tExpected:%s\n\tActual:%s", expected, p7.Content)
}
}
func TestVerifyEC2(t *testing.T) {
fixture := UnmarshalTestFixture(EC2IdentityDocumentFixture)
p7, err := Parse(fixture.Input)
if err != nil {
t.Errorf("Parse encountered unexpected error: %v", err)
}
p7.Certificates = []*x509.Certificate{fixture.Certificate}
if err := p7.Verify(); err != nil {
t.Errorf("Verify failed with error: %v", err)
}
}
func TestDecrypt(t *testing.T) {
fixture := UnmarshalTestFixture(EncryptedTestFixture)
p7, err := Parse(fixture.Input)
if err != nil {
t.Fatal(err)
}
content, err := p7.Decrypt(fixture.Certificate, fixture.PrivateKey)
if err != nil {
t.Errorf("Cannot Decrypt with error: %v", err)
}
expected := []byte("This is a test")
if bytes.Compare(content, expected) != 0 {
t.Errorf("Decrypted result does not match.\n\tExpected:%s\n\tActual:%s", expected, content)
}
}
func TestDegenerateCertificate(t *testing.T) {
cert, err := createTestCertificate()
if err != nil {
t.Fatal(err)
}
deg, err := DegenerateCertificate(cert.Certificate.Raw)
if err != nil {
t.Fatal(err)
}
fmt.Printf("=== BEGIN DEGENERATE CERT ===\n% X\n=== END DEGENERATE CERT ===\n", deg)
}
func TestSign(t *testing.T) {
cert, err := createTestCertificate()
if err != nil {
t.Fatal(err)
}
content := []byte("Hello World")
toBeSigned, err := NewSignedData(content)
if err != nil {
t.Fatalf("Cannot initialize signed data: %s", err)
}
if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil {
t.Fatalf("Cannot add signer: %s", err)
}
signed, err := toBeSigned.Finish()
if err != nil {
t.Fatalf("Cannot finish signing data: %s", err)
}
fmt.Printf("=== BEGIN SIGNED RESULT ===\n% X\n=== END SIGNED RESULT ===\n", signed)
p7, err := Parse(signed)
if err != nil {
t.Fatalf("Cannot parse our signed data: %s", err)
}
if bytes.Compare(content, p7.Content) != 0 {
t.Errorf("Our content was not in the parsed data:\n\tExpected: %s\n\tActual: %s", content, p7.Content)
}
if err := p7.Verify(); err != nil {
t.Errorf("Cannot verify our signed data: %s", err)
}
}
func TestEncrypt(t *testing.T) {
plaintext := []byte("Hello Secret World!")
cert, err := createTestCertificate()
if err != nil {
t.Fatal(err)
}
encrypted, err := Encrypt(plaintext, []*x509.Certificate{cert.Certificate})
if err != nil {
t.Fatal(err)
}
p7, err := Parse(encrypted)
if err != nil {
t.Fatalf("cannot Parse encrypted result: %s", err)
}
result, err := p7.Decrypt(cert.Certificate, cert.PrivateKey)
if err != nil {
t.Fatalf("cannot Decrypt encrypted result: %s", err)
}
if bytes.Compare(plaintext, result) != 0 {
t.Errorf("encrypted data does not match plaintext:\n\tExpected: %s\n\tActual: %s", plaintext, result)
}
}
func TestUnmarshalSignedAttribute(t *testing.T) {
cert, err := createTestCertificate()
if err != nil {
t.Fatal(err)
}
content := []byte("Hello World")
toBeSigned, err := NewSignedData(content)
if err != nil {
t.Fatalf("Cannot initialize signed data: %s", err)
}
oidTest := asn1.ObjectIdentifier{2, 3, 4, 5, 6, 7}
testValue := "TestValue"
if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{
ExtraSignedAttributes: []Attribute{Attribute{Type: oidTest, Value: testValue}},
}); err != nil {
t.Fatalf("Cannot add signer: %s", err)
}
signed, err := toBeSigned.Finish()
if err != nil {
t.Fatalf("Cannot finish signing data: %s", err)
}
p7, err := Parse(signed)
var actual string
err = p7.UnmarshalSignedAttribute(oidTest, &actual)
if err != nil {
t.Fatalf("Cannot unmarshal test value: %s", err)
}
if testValue != actual {
t.Errorf("Attribute does not match test value\n\tExpected: %s\n\tActual: %s", testValue, actual)
}
}
func TestPad(t *testing.T) {
tests := []struct {
Original []byte
Expected []byte
BlockSize int
}{
{[]byte{0x1, 0x2, 0x3, 0x10}, []byte{0x1, 0x2, 0x3, 0x10, 0x4, 0x4, 0x4, 0x4}, 8},
{[]byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0}, []byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8}, 8},
}
for _, test := range tests {
padded, err := pad(test.Original, test.BlockSize)
if err != nil {
t.Errorf("pad encountered error: %s", err)
continue
}
if bytes.Compare(test.Expected, padded) != 0 {
t.Errorf("pad results mismatch:\n\tExpected: %X\n\tActual: %X", test.Expected, padded)
}
}
}
type certKeyPair struct {
Certificate *x509.Certificate
PrivateKey *rsa.PrivateKey
}
func createTestCertificate() (certKeyPair, error) {
signer, err := createTestCertificateByIssuer("Eddard Stark", nil)
if err != nil {
return certKeyPair{}, err
}
pair, err := createTestCertificateByIssuer("Jon Snow", signer)
if err != nil {
return certKeyPair{}, err
}
return *pair, nil
}
func createTestCertificateByIssuer(name string, issuer *certKeyPair) (*certKeyPair, error) {
priv, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return nil, err
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 32)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, err
}
template := x509.Certificate{
SerialNumber: serialNumber,
SignatureAlgorithm: x509.SHA256WithRSA,
Subject: pkix.Name{
CommonName: name,
Organization: []string{"Acme Co"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
}
var issuerCert *x509.Certificate
var issuerKey crypto.PrivateKey
if issuer != nil {
issuerCert = issuer.Certificate
issuerKey = issuer.PrivateKey
} else {
issuerCert = &template
issuerKey = priv
}
cert, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, priv.Public(), issuerKey)
if err != nil {
return nil, err
}
leaf, err := x509.ParseCertificate(cert)
if err != nil {
return nil, err
}
return &certKeyPair{
Certificate: leaf,
PrivateKey: priv,
}, nil
}
type TestFixture struct {
Input []byte
Certificate *x509.Certificate
PrivateKey *rsa.PrivateKey
}
func UnmarshalTestFixture(testPEMBlock string) TestFixture {
var result TestFixture
var derBlock *pem.Block
var pemBlock = []byte(testPEMBlock)
for {
derBlock, pemBlock = pem.Decode(pemBlock)
if derBlock == nil {
break
}
switch derBlock.Type {
case "PKCS7":
result.Input = derBlock.Bytes
case "CERTIFICATE":
result.Certificate, _ = x509.ParseCertificate(derBlock.Bytes)
case "PRIVATE KEY":
result.PrivateKey, _ = x509.ParsePKCS1PrivateKey(derBlock.Bytes)
}
}
return result
}
func MarshalTestFixture(t TestFixture, w io.Writer) {
if t.Input != nil {
pem.Encode(w, &pem.Block{Type: "PKCS7", Bytes: t.Input})
}
if t.Certificate != nil {
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: t.Certificate.Raw})
}
if t.PrivateKey != nil {
pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(t.PrivateKey)})
}
}
var SignedTestFixture = `
-----BEGIN PKCS7-----
MIIDVgYJKoZIhvcNAQcCoIIDRzCCA0MCAQExCTAHBgUrDgMCGjAcBgkqhkiG9w0B
BwGgDwQNV2UgdGhlIFBlb3BsZaCCAdkwggHVMIIBQKADAgECAgRpuDctMAsGCSqG
SIb3DQEBCzApMRAwDgYDVQQKEwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3Rh
cmswHhcNMTUwNTA2MDQyNDQ4WhcNMTYwNTA2MDQyNDQ4WjAlMRAwDgYDVQQKEwdB
Y21lIENvMREwDwYDVQQDEwhKb24gU25vdzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
gYkCgYEAqr+tTF4mZP5rMwlXp1y+crRtFpuLXF1zvBZiYMfIvAHwo1ta8E1IcyEP
J1jIiKMcwbzeo6kAmZzIJRCTezq9jwXUsKbQTvcfOH9HmjUmXBRWFXZYoQs/OaaF
a45deHmwEeMQkuSWEtYiVKKZXtJOtflKIT3MryJEDiiItMkdybUCAwEAAaMSMBAw
DgYDVR0PAQH/BAQDAgCgMAsGCSqGSIb3DQEBCwOBgQDK1EweZWRL+f7Z+J0kVzY8
zXptcBaV4Lf5wGZJLJVUgp33bpLNpT3yadS++XQJ+cvtW3wADQzBSTMduyOF8Zf+
L7TjjrQ2+F2HbNbKUhBQKudxTfv9dJHdKbD+ngCCdQJYkIy2YexsoNG0C8nQkggy
axZd/J69xDVx6pui3Sj8sDGCATYwggEyAgEBMDEwKTEQMA4GA1UEChMHQWNtZSBD
bzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrAgRpuDctMAcGBSsOAwIaoGEwGAYJKoZI
hvcNAQkDMQsGCSqGSIb3DQEHATAgBgkqhkiG9w0BCQUxExcRMTUwNTA2MDAyNDQ4
LTA0MDAwIwYJKoZIhvcNAQkEMRYEFG9D7gcTh9zfKiYNJ1lgB0yTh4sZMAsGCSqG
SIb3DQEBAQSBgFF3sGDU9PtXty/QMtpcFa35vvIOqmWQAIZt93XAskQOnBq4OloX
iL9Ct7t1m4pzjRm0o9nDkbaSLZe7HKASHdCqijroScGlI8M+alJ8drHSFv6ZIjnM
FIwIf0B2Lko6nh9/6mUXq7tbbIHa3Gd1JUVire/QFFtmgRXMbXYk8SIS
-----END PKCS7-----
-----BEGIN CERTIFICATE-----
MIIB1TCCAUCgAwIBAgIEabg3LTALBgkqhkiG9w0BAQswKTEQMA4GA1UEChMHQWNt
ZSBDbzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrMB4XDTE1MDUwNjA0MjQ0OFoXDTE2
MDUwNjA0MjQ0OFowJTEQMA4GA1UEChMHQWNtZSBDbzERMA8GA1UEAxMISm9uIFNu
b3cwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKq/rUxeJmT+azMJV6dcvnK0
bRabi1xdc7wWYmDHyLwB8KNbWvBNSHMhDydYyIijHMG83qOpAJmcyCUQk3s6vY8F
1LCm0E73Hzh/R5o1JlwUVhV2WKELPzmmhWuOXXh5sBHjEJLklhLWIlSimV7STrX5
SiE9zK8iRA4oiLTJHcm1AgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIAoDALBgkqhkiG
9w0BAQsDgYEAytRMHmVkS/n+2fidJFc2PM16bXAWleC3+cBmSSyVVIKd926SzaU9
8mnUvvl0CfnL7Vt8AA0MwUkzHbsjhfGX/i+04460Nvhdh2zWylIQUCrncU37/XSR
3Smw/p4AgnUCWJCMtmHsbKDRtAvJ0JIIMmsWXfyevcQ1ceqbot0o/LA=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIICXgIBAAKBgQCqv61MXiZk/mszCVenXL5ytG0Wm4tcXXO8FmJgx8i8AfCjW1rw
TUhzIQ8nWMiIoxzBvN6jqQCZnMglEJN7Or2PBdSwptBO9x84f0eaNSZcFFYVdlih
Cz85poVrjl14ebAR4xCS5JYS1iJUople0k61+UohPcyvIkQOKIi0yR3JtQIDAQAB
AoGBAIPLCR9N+IKxodq11lNXEaUFwMHXc1zqwP8no+2hpz3+nVfplqqubEJ4/PJY
5AgbJoIfnxVhyBXJXu7E+aD/OPneKZrgp58YvHKgGvvPyJg2gpC/1Fh0vQB0HNpI
1ZzIZUl8ZTUtVgtnCBUOh5JGI4bFokAqrT//Uvcfd+idgxqBAkEA1ZbP/Kseld14
qbWmgmU5GCVxsZRxgR1j4lG3UVjH36KXMtRTm1atAam1uw3OEGa6Y3ANjpU52FaB
Hep5rkk4FQJBAMynMo1L1uiN5GP+KYLEF5kKRxK+FLjXR0ywnMh+gpGcZDcOae+J
+t1gLoWBIESH/Xt639T7smuSfrZSA9V0EyECQA8cvZiWDvLxmaEAXkipmtGPjKzQ
4PsOtkuEFqFl07aKDYKmLUg3aMROWrJidqsIabWxbvQgsNgSvs38EiH3wkUCQQCg
ndxb7piVXb9RBwm3OoU2tE1BlXMX+sVXmAkEhd2dwDsaxrI3sHf1xGXem5AimQRF
JBOFyaCnMotGNioSHY5hAkEAxyXcNixQ2RpLXJTQZtwnbk0XDcbgB+fBgXnv/4f3
BCvcu85DqJeJyQv44Oe1qsXEX9BfcQIOVaoep35RPlKi9g==
-----END PRIVATE KEY-----`
// Content is "This is a test"
var EncryptedTestFixture = `
-----BEGIN PKCS7-----
MIIBFwYJKoZIhvcNAQcDoIIBCDCCAQQCAQAxgcowgccCAQAwMjApMRAwDgYDVQQK
EwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3RhcmsCBQDL+CvWMAsGCSqGSIb3
DQEBAQSBgKyP/5WlRTZD3dWMrLOX6QRNDrXEkQjhmToRwFZdY3LgUh25ZU0S/q4G
dHPV21Fv9lQD+q7l3vfeHw8M6Z1PKi9sHMVfxAkQpvaI96DTIT3YHtuLC1w3geCO
8eFWTq2qS4WChSuS/yhYosjA1kTkE0eLnVZcGw0z/WVuEZznkdyIMDIGCSqGSIb3
DQEHATARBgUrDgMCBwQImpKsUyMPpQigEgQQRcWWrCRXqpD5Njs0GkJl+g==
-----END PKCS7-----
-----BEGIN CERTIFICATE-----
MIIB1jCCAUGgAwIBAgIFAMv4K9YwCwYJKoZIhvcNAQELMCkxEDAOBgNVBAoTB0Fj
bWUgQ28xFTATBgNVBAMTDEVkZGFyZCBTdGFyazAeFw0xNTA1MDYwMzU2NDBaFw0x
NjA1MDYwMzU2NDBaMCUxEDAOBgNVBAoTB0FjbWUgQ28xETAPBgNVBAMTCEpvbiBT
bm93MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK6NU0R0eiCYVquU4RcjKc
LzGfx0aa1lMr2TnLQUSeLFZHFxsyyMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg
8+Zg2r8xnnney7abxcuv0uATWSIeKlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP
+Zxp2ni5qHNraf3wE2VPIQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAKAwCwYJKoZI
hvcNAQELA4GBAIr2F7wsqmEU/J/kLyrCgEVXgaV/sKZq4pPNnzS0tBYk8fkV3V18
sBJyHKRLL/wFZASvzDcVGCplXyMdAOCyfd8jO3F9Ac/xdlz10RrHJT75hNu3a7/n
9KNwKhfN4A1CQv2x372oGjRhCW5bHNCWx4PIVeNzCyq/KZhyY9sxHE6f
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIICXgIBAAKBgQDK6NU0R0eiCYVquU4RcjKcLzGfx0aa1lMr2TnLQUSeLFZHFxsy
yMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg8+Zg2r8xnnney7abxcuv0uATWSIe
KlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP+Zxp2ni5qHNraf3wE2VPIQIDAQAB
AoGBALyvnSt7KUquDen7nXQtvJBudnf9KFPt//OjkdHHxNZNpoF/JCSqfQeoYkeu
MdAVYNLQGMiRifzZz4dDhA9xfUAuy7lcGQcMCxEQ1dwwuFaYkawbS0Tvy2PFlq2d
H5/HeDXU4EDJ3BZg0eYj2Bnkt1sJI35UKQSxblQ0MY2q0uFBAkEA5MMOogkgUx1C
67S1tFqMUSM8D0mZB0O5vOJZC5Gtt2Urju6vywge2ArExWRXlM2qGl8afFy2SgSv
Xk5eybcEiQJBAOMRwwbEoW5NYHuFFbSJyWll4n71CYuWuQOCzehDPyTb80WFZGLV
i91kFIjeERyq88eDE5xVB3ZuRiXqaShO/9kCQQCKOEkpInaDgZSjskZvuJ47kByD
6CYsO4GIXQMMeHML8ncFH7bb6AYq5ybJVb2NTU7QLFJmfeYuhvIm+xdOreRxAkEA
o5FC5Jg2FUfFzZSDmyZ6IONUsdF/i78KDV5nRv1R+hI6/oRlWNCtTNBv/lvBBd6b
dseUE9QoaQZsn5lpILEvmQJAZ0B+Or1rAYjnbjnUhdVZoy9kC4Zov+4UH3N/BtSy
KJRWUR0wTWfZBPZ5hAYZjTBEAFULaYCXlQKsODSp0M1aQA==
-----END PRIVATE KEY-----`
var EC2IdentityDocumentFixture = `
-----BEGIN PKCS7-----
MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA
JIAEggGmewogICJwcml2YXRlSXAiIDogIjE3Mi4zMC4wLjI1MiIsCiAgImRldnBh
eVByb2R1Y3RDb2RlcyIgOiBudWxsLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1
cy1lYXN0LTFhIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3Rh
bmNlSWQiIDogImktZjc5ZmU1NmMiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVs
bCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIg
OiAiMTIxNjU5MDE0MzM0IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwK
ICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDhUMDM6MDE6MzhaIiwKICAiYXJj
aGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJy
YW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAA
AAAxggEYMIIBFAIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5n
dG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2Vi
IFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0B
CQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDgwMzAxNDRaMCMG
CSqGSIb3DQEJBDEWBBTuUc28eBXmImAautC+wOjqcFCBVjAJBgcqhkjOOAQDBC8w
LQIVAKA54NxGHWWCz5InboDmY/GHs33nAhQ6O/ZI86NwjA9Vz3RNMUJrUPU5tAAA
AAAAAA==
-----END PKCS7-----
-----BEGIN CERTIFICATE-----
MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw
FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD
VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z
ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u
IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl
cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e
ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3
VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P
hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j
k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U
hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF
lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf
MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW
MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw
vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw
7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K
-----END CERTIFICATE-----`

File diff suppressed because it is too large Load diff

View file

@ -181,6 +181,10 @@
<li<%= sidebar_current("docs-auth-userpass") %>>
<a href="/docs/auth/userpass.html">Username &amp; Password</a>
</li>
<li<%= sidebar_current("docs-auth-aws") %>>
<a href="/docs/auth/aws.html">AWS EC2 Auth</a>
</li>
</ul>
</li>