mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-10 17:32:29 -04:00
Merge pull request #1300 from hashicorp/aws-auth-backend
AWS EC2 instances authentication backend
This commit is contained in:
commit
943789a11e
29 changed files with 7373 additions and 30 deletions
177
builtin/credential/aws/backend.go
Normal file
177
builtin/credential/aws/backend.go
Normal 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.
|
||||
`
|
||||
1273
builtin/credential/aws/backend_test.go
Normal file
1273
builtin/credential/aws/backend_test.go
Normal file
File diff suppressed because it is too large
Load diff
103
builtin/credential/aws/client.go
Normal file
103
builtin/credential/aws/client.go
Normal 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
|
||||
}
|
||||
334
builtin/credential/aws/path_config_certificate.go
Normal file
334
builtin/credential/aws/path_config_certificate.go
Normal 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.
|
||||
`
|
||||
199
builtin/credential/aws/path_config_client.go
Normal file
199
builtin/credential/aws/path_config_client.go
Normal 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.
|
||||
`
|
||||
149
builtin/credential/aws/path_config_tidy_identity_whitelist.go
Normal file
149
builtin/credential/aws/path_config_tidy_identity_whitelist.go
Normal 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.
|
||||
`
|
||||
150
builtin/credential/aws/path_config_tidy_roletag_blacklist.go
Normal file
150
builtin/credential/aws/path_config_tidy_roletag_blacklist.go
Normal 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.
|
||||
`
|
||||
154
builtin/credential/aws/path_identity_whitelist.go
Normal file
154
builtin/credential/aws/path_identity_whitelist.go
Normal 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.
|
||||
`
|
||||
567
builtin/credential/aws/path_login.go
Normal file
567
builtin/credential/aws/path_login.go
Normal 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.
|
||||
`
|
||||
335
builtin/credential/aws/path_role.go
Normal file
335
builtin/credential/aws/path_role.go
Normal 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.
|
||||
`
|
||||
432
builtin/credential/aws/path_role_tag.go
Normal file
432
builtin/credential/aws/path_role_tag.go
Normal 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.
|
||||
`
|
||||
255
builtin/credential/aws/path_roletag_blacklist.go
Normal file
255
builtin/credential/aws/path_roletag_blacklist.go
Normal 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.
|
||||
`
|
||||
96
builtin/credential/aws/path_tidy_identity_whitelist.go
Normal file
96
builtin/credential/aws/path_tidy_identity_whitelist.go
Normal 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.
|
||||
`
|
||||
95
builtin/credential/aws/path_tidy_roletag_blacklist.go
Normal file
95
builtin/credential/aws/path_tidy_roletag_blacklist.go
Normal 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.
|
||||
`
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
24
vendor/github.com/fullsailor/pkcs7/.gitignore
generated
vendored
Normal 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
22
vendor/github.com/fullsailor/pkcs7/LICENSE
generated
vendored
Normal 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
7
vendor/github.com/fullsailor/pkcs7/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# pkcs7
|
||||
|
||||
[](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
228
vendor/github.com/fullsailor/pkcs7/ber.go
generated
vendored
Normal 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
61
vendor/github.com/fullsailor/pkcs7/ber_test.go
generated
vendored
Normal 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
776
vendor/github.com/fullsailor/pkcs7/pkcs7.go
generated
vendored
Normal 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
410
vendor/github.com/fullsailor/pkcs7/pkcs7_test.go
generated
vendored
Normal 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-----`
|
||||
1455
website/source/docs/auth/aws.html.md
Normal file
1455
website/source/docs/auth/aws.html.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -181,6 +181,10 @@
|
|||
<li<%= sidebar_current("docs-auth-userpass") %>>
|
||||
<a href="/docs/auth/userpass.html">Username & Password</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-auth-aws") %>>
|
||||
<a href="/docs/auth/aws.html">AWS EC2 Auth</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue