2019-04-24 19:12:36 +00:00
package provisioner
import (
2019-07-29 22:54:07 +00:00
"context"
2019-04-24 19:12:36 +00:00
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
2021-11-12 23:46:34 +00:00
"io"
2019-04-24 19:12:36 +00:00
"net"
"net/http"
2021-11-12 23:46:34 +00:00
"os"
2019-04-24 19:12:36 +00:00
"strings"
"time"
"github.com/pkg/errors"
2022-04-21 23:20:38 +00:00
2020-08-24 21:44:11 +00:00
"go.step.sm/crypto/jose"
2020-08-10 18:26:51 +00:00
"go.step.sm/crypto/sshutil"
2020-08-05 23:02:46 +00:00
"go.step.sm/crypto/x509util"
2022-09-30 00:16:26 +00:00
"go.step.sm/linkedca"
2022-04-21 23:20:38 +00:00
"github.com/smallstep/certificates/errs"
2023-07-24 05:29:22 +00:00
"github.com/smallstep/certificates/webhook"
2023-08-17 17:37:53 +00:00
_ "embed"
2019-04-24 19:12:36 +00:00
)
// awsIssuer is the string used as issuer in the generated tokens.
const awsIssuer = "ec2.amazonaws.com"
// awsIdentityURL is the url used to retrieve the instance identity document.
const awsIdentityURL = "http://169.254.169.254/latest/dynamic/instance-identity/document"
// awsSignatureURL is the url used to retrieve the instance identity signature.
const awsSignatureURL = "http://169.254.169.254/latest/dynamic/instance-identity/signature"
2020-05-19 22:42:12 +00:00
// awsAPITokenURL is the url used to get the IMDSv2 API token
2022-09-21 04:48:04 +00:00
const awsAPITokenURL = "http://169.254.169.254/latest/api/token" //nolint:gosec // no credentials here
2020-05-19 22:42:12 +00:00
// awsAPITokenTTL is the default TTL to use when requesting IMDSv2 API tokens
// -- we keep this short-lived since we get a new token with every call to readURL()
const awsAPITokenTTL = "30"
// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request
2022-09-21 04:48:04 +00:00
const awsMetadataTokenHeader = "X-aws-ec2-metadata-token" //nolint:gosec // no credentials here
2020-05-19 22:42:12 +00:00
// awsMetadataTokenTTLHeader is the header used to indicate the token TTL requested
2022-09-21 04:48:04 +00:00
const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds" //nolint:gosec // no credentials here
2020-05-19 22:42:12 +00:00
2019-04-24 19:12:36 +00:00
// awsCertificate is the certificate used to validate the instance identity
2023-08-17 17:37:53 +00:00
// signature. It is embedded in the binary at compile time.
2023-05-25 12:47:13 +00:00
//
2023-08-17 17:37:53 +00:00
//go:embed aws_certificates.pem
var awsCertificate string
2019-04-24 19:12:36 +00:00
// awsSignatureAlgorithm is the signature algorithm used to verify the identity
// document signature.
const awsSignatureAlgorithm = x509 . SHA256WithRSA
type awsConfig struct {
identityURL string
signatureURL string
2020-05-20 13:03:35 +00:00
tokenURL string
tokenTTL string
2020-10-14 00:51:24 +00:00
certificates [ ] * x509 . Certificate
2019-04-24 19:12:36 +00:00
signatureAlgorithm x509 . SignatureAlgorithm
}
2020-10-14 00:51:24 +00:00
func newAWSConfig ( certPath string ) ( * awsConfig , error ) {
var certBytes [ ] byte
if certPath == "" {
certBytes = [ ] byte ( awsCertificate )
} else {
2021-11-12 23:46:34 +00:00
if b , err := os . ReadFile ( certPath ) ; err == nil {
2020-10-14 00:51:24 +00:00
certBytes = b
} else {
return nil , errors . Wrapf ( err , "error reading %s" , certPath )
}
2019-04-24 19:12:36 +00:00
}
2020-10-14 00:51:24 +00:00
// Read all the certificates.
var certs [ ] * x509 . Certificate
for len ( certBytes ) > 0 {
var block * pem . Block
block , certBytes = pem . Decode ( certBytes )
if block == nil {
break
}
if block . Type != "CERTIFICATE" || len ( block . Headers ) != 0 {
continue
}
cert , err := x509 . ParseCertificate ( block . Bytes )
if err != nil {
return nil , errors . Wrap ( err , "error parsing AWS IID certificate" )
}
certs = append ( certs , cert )
2019-04-24 19:12:36 +00:00
}
2020-10-14 00:51:24 +00:00
if len ( certs ) == 0 {
return nil , errors . New ( "error parsing AWS IID certificate: no certificates found" )
}
2019-04-24 19:12:36 +00:00
return & awsConfig {
identityURL : awsIdentityURL ,
signatureURL : awsSignatureURL ,
2020-05-20 13:03:35 +00:00
tokenURL : awsAPITokenURL ,
tokenTTL : awsAPITokenTTL ,
2020-10-14 00:51:24 +00:00
certificates : certs ,
2019-04-24 19:12:36 +00:00
signatureAlgorithm : awsSignatureAlgorithm ,
} , nil
}
type awsPayload struct {
jose . Claims
Amazon awsAmazonPayload ` json:"amazon" `
SANs [ ] string ` json:"sans" `
document awsInstanceIdentityDocument
}
type awsAmazonPayload struct {
Document [ ] byte ` json:"document" `
Signature [ ] byte ` json:"signature" `
}
type awsInstanceIdentityDocument struct {
AccountID string ` json:"accountId" `
Architecture string ` json:"architecture" `
AvailabilityZone string ` json:"availabilityZone" `
BillingProducts [ ] string ` json:"billingProducts" `
DevpayProductCodes [ ] string ` json:"devpayProductCodes" `
ImageID string ` json:"imageId" `
InstanceID string ` json:"instanceId" `
InstanceType string ` json:"instanceType" `
KernelID string ` json:"kernelId" `
PendingTime time . Time ` json:"pendingTime" `
PrivateIP string ` json:"privateIp" `
RamdiskID string ` json:"ramdiskId" `
Region string ` json:"region" `
Version string ` json:"version" `
}
// AWS is the provisioner that supports identity tokens created from the Amazon
// Web Services Instance Identity Documents.
//
// If DisableCustomSANs is true, only the internal DNS and IP will be added as a
// SAN. By default it will accept any SAN in the CSR.
//
// If DisableTrustOnFirstUse is true, multiple sign request for this provisioner
// with the same instance will be accepted. By default only the first request
// will be accepted.
2019-06-05 18:04:00 +00:00
//
2019-06-07 18:24:56 +00:00
// If InstanceAge is set, only the instances with a pendingTime within the given
// period will be accepted.
2019-06-05 18:04:00 +00:00
//
2020-10-14 00:51:24 +00:00
// IIDRoots can be used to specify a path to the certificates used to verify the
// identity certificate signature.
//
2019-06-05 18:04:00 +00:00
// Amazon Identity docs are available at
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
2019-04-24 19:12:36 +00:00
type AWS struct {
2019-10-28 18:50:43 +00:00
* base
2021-05-20 01:23:20 +00:00
ID string ` json:"-" `
2020-07-31 01:44:52 +00:00
Type string ` json:"type" `
Name string ` json:"name" `
Accounts [ ] string ` json:"accounts" `
DisableCustomSANs bool ` json:"disableCustomSANs" `
DisableTrustOnFirstUse bool ` json:"disableTrustOnFirstUse" `
2020-05-20 17:15:51 +00:00
IMDSVersions [ ] string ` json:"imdsVersions" `
2020-07-31 01:44:52 +00:00
InstanceAge Duration ` json:"instanceAge,omitempty" `
2020-10-14 00:51:24 +00:00
IIDRoots string ` json:"iidRoots,omitempty" `
2020-07-31 01:44:52 +00:00
Claims * Claims ` json:"claims,omitempty" `
Options * Options ` json:"options,omitempty" `
2019-04-24 19:12:36 +00:00
config * awsConfig
2022-03-10 02:43:45 +00:00
ctl * Controller
2019-04-24 19:12:36 +00:00
}
// GetID returns the provisioner unique identifier.
func ( p * AWS ) GetID ( ) string {
2021-05-20 01:23:20 +00:00
if p . ID != "" {
return p . ID
}
return p . GetIDForToken ( )
}
// GetIDForToken returns an identifier that will be used to load the provisioner
// from a token.
func ( p * AWS ) GetIDForToken ( ) string {
2019-06-06 19:49:51 +00:00
return "aws/" + p . Name
2019-04-24 19:12:36 +00:00
}
// GetTokenID returns the identifier of the token.
func ( p * AWS ) GetTokenID ( token string ) ( string , error ) {
payload , err := p . authorizeToken ( token )
if err != nil {
return "" , err
}
// If TOFU is disabled create an ID for the token, so it cannot be reused.
// The timestamps, document and signatures should be mostly unique.
if p . DisableTrustOnFirstUse {
sum := sha256 . Sum256 ( [ ] byte ( token ) )
return strings . ToLower ( hex . EncodeToString ( sum [ : ] ) ) , nil
}
2020-12-17 22:52:34 +00:00
// Use provisioner + instance-id as the identifier.
2021-05-03 19:48:20 +00:00
unique := fmt . Sprintf ( "%s.%s" , p . GetIDForToken ( ) , payload . document . InstanceID )
2020-12-17 22:52:34 +00:00
sum := sha256 . Sum256 ( [ ] byte ( unique ) )
return strings . ToLower ( hex . EncodeToString ( sum [ : ] ) ) , nil
2019-04-24 19:12:36 +00:00
}
// GetName returns the name of the provisioner.
func ( p * AWS ) GetName ( ) string {
return p . Name
}
// GetType returns the type of provisioner.
func ( p * AWS ) GetType ( ) Type {
return TypeAWS
}
// GetEncryptedKey is not available in an AWS provisioner.
2021-10-08 18:59:57 +00:00
func ( p * AWS ) GetEncryptedKey ( ) ( kid , key string , ok bool ) {
2019-04-24 19:12:36 +00:00
return "" , "" , false
}
// GetIdentityToken retrieves the identity document and it's signature and
// generates a token with them.
2019-07-15 22:52:36 +00:00
func ( p * AWS ) GetIdentityToken ( subject , caURL string ) ( string , error ) {
2019-04-24 19:12:36 +00:00
// Initialize the config if this method is used from the cli.
if err := p . assertConfig ( ) ; err != nil {
return "" , err
}
var idoc awsInstanceIdentityDocument
doc , err := p . readURL ( p . config . identityURL )
if err != nil {
2020-05-20 18:43:25 +00:00
return "" , errors . Wrap ( err , "error retrieving identity document:\n Are you in an AWS VM?\n Is the metadata service enabled?\n Are you using the proper metadata service version?" )
2019-04-24 19:12:36 +00:00
}
if err := json . Unmarshal ( doc , & idoc ) ; err != nil {
return "" , errors . Wrap ( err , "error unmarshaling identity document" )
}
sig , err := p . readURL ( p . config . signatureURL )
if err != nil {
2020-05-20 18:43:25 +00:00
return "" , errors . Wrap ( err , "error retrieving identity document:\n Are you in an AWS VM?\n Is the metadata service enabled?\n Are you using the proper metadata service version?" )
2019-04-24 19:12:36 +00:00
}
signature , err := base64 . StdEncoding . DecodeString ( string ( sig ) )
if err != nil {
return "" , errors . Wrap ( err , "error decoding identity document signature" )
}
if err := p . checkSignature ( doc , signature ) ; err != nil {
return "" , err
}
2021-05-03 19:48:20 +00:00
audience , err := generateSignAudience ( caURL , p . GetIDForToken ( ) )
2019-06-06 19:49:51 +00:00
if err != nil {
return "" , err
}
2019-04-24 19:12:36 +00:00
// Create unique ID for Trust On First Use (TOFU). Only the first instance
// per provisioner is allowed as we don't have a way to trust the given
// sans.
2021-05-03 19:48:20 +00:00
unique := fmt . Sprintf ( "%s.%s" , p . GetIDForToken ( ) , idoc . InstanceID )
2019-04-24 19:12:36 +00:00
sum := sha256 . Sum256 ( [ ] byte ( unique ) )
// Create a JWT from the identity document
signer , err := jose . NewSigner (
jose . SigningKey { Algorithm : jose . HS256 , Key : signature } ,
new ( jose . SignerOptions ) . WithType ( "JWT" ) ,
)
2019-04-24 21:59:01 +00:00
if err != nil {
return "" , errors . Wrap ( err , "error creating signer" )
}
2019-04-24 19:12:36 +00:00
now := time . Now ( )
payload := awsPayload {
Claims : jose . Claims {
Issuer : awsIssuer ,
2019-07-15 22:52:36 +00:00
Subject : subject ,
2019-06-06 19:49:51 +00:00
Audience : [ ] string { audience } ,
2019-04-24 19:12:36 +00:00
Expiry : jose . NewNumericDate ( now . Add ( 5 * time . Minute ) ) ,
NotBefore : jose . NewNumericDate ( now ) ,
IssuedAt : jose . NewNumericDate ( now ) ,
ID : strings . ToLower ( hex . EncodeToString ( sum [ : ] ) ) ,
} ,
Amazon : awsAmazonPayload {
Document : doc ,
Signature : signature ,
} ,
}
tok , err := jose . Signed ( signer ) . Claims ( payload ) . CompactSerialize ( )
if err != nil {
2020-10-14 00:51:24 +00:00
return "" , errors . Wrap ( err , "error serializing token" )
2019-04-24 19:12:36 +00:00
}
return tok , nil
}
// Init validates and initializes the AWS provisioner.
func ( p * AWS ) Init ( config Config ) ( err error ) {
switch {
case p . Type == "" :
return errors . New ( "provisioner type cannot be empty" )
case p . Name == "" :
return errors . New ( "provisioner name cannot be empty" )
2019-06-04 23:31:33 +00:00
case p . InstanceAge . Value ( ) < 0 :
return errors . New ( "provisioner instanceAge cannot be negative" )
2019-04-24 19:12:36 +00:00
}
2022-03-10 02:43:45 +00:00
2019-04-24 19:12:36 +00:00
// Add default config
2020-10-14 00:51:24 +00:00
if p . config , err = newAWSConfig ( p . IIDRoots ) ; err != nil {
2019-04-24 19:12:36 +00:00
return err
}
2020-05-20 17:24:45 +00:00
// validate IMDS versions
if len ( p . IMDSVersions ) == 0 {
p . IMDSVersions = [ ] string { "v2" , "v1" }
}
for _ , v := range p . IMDSVersions {
switch v {
case "v1" :
// valid
case "v2" :
// valid
default :
return errors . Errorf ( "%s: not a supported AWS Instance Metadata Service version" , v )
}
}
2022-03-10 02:43:45 +00:00
config . Audiences = config . Audiences . WithFragment ( p . GetIDForToken ( ) )
2022-04-21 23:20:38 +00:00
p . ctl , err = NewController ( p , p . Claims , config , p . Options )
2022-03-10 02:43:45 +00:00
return
2019-04-24 19:12:36 +00:00
}
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
2023-05-10 06:47:28 +00:00
func ( p * AWS ) AuthorizeSign ( _ context . Context , token string ) ( [ ] SignOption , error ) {
2019-04-24 19:12:36 +00:00
payload , err := p . authorizeToken ( token )
if err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSign" )
2019-04-24 19:12:36 +00:00
}
2019-07-29 22:54:07 +00:00
doc := payload . document
2020-07-13 19:47:26 +00:00
// Template options
data := x509util . NewTemplateData ( )
data . SetCommonName ( payload . Claims . Subject )
2020-07-21 18:41:36 +00:00
if v , err := unsafeParseSigned ( token ) ; err == nil {
data . SetToken ( v )
}
2020-07-13 19:47:26 +00:00
2019-07-15 22:52:36 +00:00
// Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR.
2019-04-24 20:05:46 +00:00
// There's no way to trust them other than TOFU.
2019-04-24 19:12:36 +00:00
var so [ ] SignOption
if p . DisableCustomSANs {
2021-10-08 18:59:57 +00:00
dnsName := fmt . Sprintf ( "ip-%s.%s.compute.internal" , strings . ReplaceAll ( doc . PrivateIP , "." , "-" ) , doc . Region )
so = append ( so ,
dnsNamesValidator ( [ ] string { dnsName } ) ,
ipAddressesValidator ( [ ] net . IP {
net . ParseIP ( doc . PrivateIP ) ,
} ) ,
emailAddressesValidator ( nil ) ,
urisValidator ( nil ) ,
)
2020-07-13 23:09:40 +00:00
// Template options
data . SetSANs ( [ ] string { dnsName , doc . PrivateIP } )
2019-04-24 19:12:36 +00:00
}
2020-07-13 19:47:26 +00:00
templateOptions , err := CustomTemplateOptions ( p . Options , data , x509util . DefaultIIDLeafTemplate )
if err != nil {
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSign" )
}
2019-04-24 19:12:36 +00:00
return append ( so ,
2022-03-22 02:21:40 +00:00
p ,
2020-07-13 19:47:26 +00:00
templateOptions ,
2019-09-05 01:31:09 +00:00
// modifiers / withOptions
2023-07-20 17:59:38 +00:00
newProvisionerExtensionOption ( TypeAWS , p . Name , doc . AccountID , "InstanceID" , doc . InstanceID ) . WithControllerOptions ( p . ctl ) ,
2022-03-10 02:43:45 +00:00
profileDefaultDuration ( p . ctl . Claimer . DefaultTLSCertDuration ( ) ) ,
2019-09-05 01:31:09 +00:00
// validators
2019-08-27 00:52:49 +00:00
defaultPublicKeyValidator { } ,
2019-07-15 22:52:36 +00:00
commonNameValidator ( payload . Claims . Subject ) ,
2022-03-10 02:43:45 +00:00
newValidityValidator ( p . ctl . Claimer . MinTLSCertDuration ( ) , p . ctl . Claimer . MaxTLSCertDuration ( ) ) ,
2022-05-05 10:32:53 +00:00
newX509NamePolicyValidator ( p . ctl . getPolicy ( ) . getX509 ( ) ) ,
2023-07-24 05:29:22 +00:00
p . ctl . newWebhookController (
data ,
linkedca . Webhook_X509 ,
webhook . WithAuthorizationPrincipal ( doc . InstanceID ) ,
) ,
2019-04-24 19:12:36 +00:00
) , nil
}
2019-10-28 18:50:43 +00:00
// AuthorizeRenew returns an error if the renewal is disabled.
// NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func ( p * AWS ) AuthorizeRenew ( ctx context . Context , cert * x509 . Certificate ) error {
2022-03-10 02:43:45 +00:00
return p . ctl . AuthorizeRenew ( ctx , cert )
2019-04-24 19:12:36 +00:00
}
// assertConfig initializes the config if it has not been initialized
func ( p * AWS ) assertConfig ( ) ( err error ) {
if p . config != nil {
return
}
2020-10-14 00:51:24 +00:00
p . config , err = newAWSConfig ( p . IIDRoots )
2019-04-24 19:12:36 +00:00
return err
}
// checkSignature returns an error if the signature is not valid.
func ( p * AWS ) checkSignature ( signed , signature [ ] byte ) error {
2020-10-14 00:51:24 +00:00
for _ , crt := range p . config . certificates {
if err := crt . CheckSignature ( p . config . signatureAlgorithm , signed , signature ) ; err == nil {
return nil
}
2019-04-24 19:12:36 +00:00
}
2020-10-14 00:51:24 +00:00
return errors . New ( "error validating identity document signature" )
2019-04-24 19:12:36 +00:00
}
// readURL does a GET request to the given url and returns the body. It's not
// using pkg/errors to avoid verbose errors, the caller should use it and write
// the appropriate error.
func ( p * AWS ) readURL ( url string ) ( [ ] byte , error ) {
2020-05-20 17:15:51 +00:00
var resp * http . Response
var err error
2021-08-27 00:55:42 +00:00
// Initialize IMDS versions when this is called from the cli.
if len ( p . IMDSVersions ) == 0 {
p . IMDSVersions = [ ] string { "v2" , "v1" }
}
2020-05-20 17:15:51 +00:00
for _ , v := range p . IMDSVersions {
switch v {
case "v1" :
resp , err = p . readURLv1 ( url )
if err == nil && resp . StatusCode < 400 {
return p . readResponseBody ( resp )
}
case "v2" :
resp , err = p . readURLv2 ( url )
if err == nil && resp . StatusCode < 400 {
return p . readResponseBody ( resp )
}
default :
return nil , fmt . Errorf ( "%s: not a supported AWS Instance Metadata Service version" , v )
}
2020-07-22 23:52:06 +00:00
if resp != nil {
resp . Body . Close ( )
}
2020-05-20 17:15:51 +00:00
}
// all versions have been exhausted and we haven't returned successfully yet so pass
// the error on to the caller
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
}
2022-03-24 12:10:49 +00:00
return nil , fmt . Errorf ( "request for metadata returned non-successful status code %d" ,
2020-05-20 17:15:51 +00:00
resp . StatusCode )
}
func ( p * AWS ) readURLv1 ( url string ) ( * http . Response , error ) {
client := http . Client { }
2020-05-19 22:42:12 +00:00
2020-07-23 01:39:46 +00:00
req , err := http . NewRequest ( http . MethodGet , url , http . NoBody )
2020-05-20 17:15:51 +00:00
if err != nil {
return nil , err
}
resp , err := client . Do ( req )
if err != nil {
return nil , err
}
return resp , nil
}
func ( p * AWS ) readURLv2 ( url string ) ( * http . Response , error ) {
client := http . Client { }
// first get the token
2021-11-12 23:46:34 +00:00
req , err := http . NewRequest ( http . MethodPut , p . config . tokenURL , http . NoBody )
2020-05-19 22:42:12 +00:00
if err != nil {
return nil , err
}
2020-05-20 13:03:35 +00:00
req . Header . Set ( awsMetadataTokenTTLHeader , p . config . tokenTTL )
2020-05-20 17:15:51 +00:00
resp , err := client . Do ( req )
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
defer resp . Body . Close ( )
if resp . StatusCode >= 400 {
2022-03-24 12:10:49 +00:00
return nil , fmt . Errorf ( "request for API token returned non-successful status code %d" , resp . StatusCode )
2020-05-20 03:57:09 +00:00
}
2021-11-12 23:46:34 +00:00
token , err := io . ReadAll ( resp . Body )
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
2020-05-19 22:42:12 +00:00
}
2020-05-20 17:15:51 +00:00
// now make the request
2020-07-23 01:39:46 +00:00
req , err = http . NewRequest ( http . MethodGet , url , http . NoBody )
2020-05-19 22:42:12 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
req . Header . Set ( awsMetadataTokenHeader , string ( token ) )
resp , err = client . Do ( req )
2020-05-19 22:42:12 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
return resp , nil
}
func ( p * AWS ) readResponseBody ( resp * http . Response ) ( [ ] byte , error ) {
defer resp . Body . Close ( )
2021-11-12 23:46:34 +00:00
b , err := io . ReadAll ( resp . Body )
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
}
return b , nil
}
// authorizeToken performs common jwt authorization actions and returns the
// claims for case specific downstream parsing.
// e.g. a Sign request will auth/validate different fields than a Revoke request.
func ( p * AWS ) authorizeToken ( token string ) ( * awsPayload , error ) {
jwt , err := jose . ParseSigned ( token )
if err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrapf ( http . StatusUnauthorized , err , "aws.authorizeToken; error parsing aws token" )
2019-04-24 19:12:36 +00:00
}
if len ( jwt . Headers ) == 0 {
2020-01-24 06:04:34 +00:00
return nil , errs . InternalServer ( "aws.authorizeToken; error parsing token, header is missing" )
2019-04-24 19:12:36 +00:00
}
var unsafeClaims awsPayload
if err := jwt . UnsafeClaimsWithoutVerification ( & unsafeClaims ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; error unmarshaling claims" )
2019-04-24 19:12:36 +00:00
}
var payload awsPayload
if err := jwt . Claims ( unsafeClaims . Amazon . Signature , & payload ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; error verifying claims" )
2019-04-24 19:12:36 +00:00
}
// Validate identity document signature
if err := p . checkSignature ( payload . Amazon . Document , payload . Amazon . Signature ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; invalid aws token signature" )
2019-04-24 19:12:36 +00:00
}
var doc awsInstanceIdentityDocument
if err := json . Unmarshal ( payload . Amazon . Document , & doc ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; error unmarshaling aws identity document" )
2019-04-24 19:12:36 +00:00
}
switch {
case doc . AccountID == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document accountId cannot be empty" )
2019-04-24 19:12:36 +00:00
case doc . InstanceID == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document instanceId cannot be empty" )
2019-04-24 19:12:36 +00:00
case doc . PrivateIP == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document privateIp cannot be empty" )
2019-04-24 19:12:36 +00:00
case doc . Region == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document region cannot be empty" )
2019-04-24 19:12:36 +00:00
}
2019-04-25 02:52:58 +00:00
// According to "rfc7519 JSON Web Token" acceptable skew should be no
// more than a few minutes.
2019-06-04 23:31:33 +00:00
now := time . Now ( ) . UTC ( )
2019-04-25 02:52:58 +00:00
if err = payload . ValidateWithLeeway ( jose . Expected {
2019-07-15 22:52:36 +00:00
Issuer : awsIssuer ,
Time : now ,
2019-04-25 02:52:58 +00:00
} , time . Minute ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrapf ( http . StatusUnauthorized , err , "aws.authorizeToken; invalid aws token" )
2019-04-25 02:52:58 +00:00
}
2019-06-06 19:49:51 +00:00
// validate audiences with the defaults
2022-03-10 02:43:45 +00:00
if ! matchesAudience ( payload . Audience , p . ctl . Audiences . Sign ) {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; invalid token - invalid audience claim (aud)" )
2019-06-06 19:49:51 +00:00
}
2019-07-15 22:52:36 +00:00
// Validate subject, it has to be known if disableCustomSANs is enabled
if p . DisableCustomSANs {
if payload . Subject != doc . InstanceID &&
payload . Subject != doc . PrivateIP &&
2021-10-08 18:59:57 +00:00
payload . Subject != fmt . Sprintf ( "ip-%s.%s.compute.internal" , strings . ReplaceAll ( doc . PrivateIP , "." , "-" ) , doc . Region ) {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; invalid token - invalid subject claim (sub)" )
2019-07-15 22:52:36 +00:00
}
}
2019-04-24 19:12:36 +00:00
// validate accounts
if len ( p . Accounts ) > 0 {
var found bool
for _ , sa := range p . Accounts {
if sa == doc . AccountID {
found = true
break
}
}
if ! found {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; invalid aws identity document - accountId is not valid" )
2019-04-24 19:12:36 +00:00
}
}
2019-06-04 23:31:33 +00:00
// validate instance age
if d := p . InstanceAge . Value ( ) ; d > 0 {
if now . Sub ( doc . PendingTime ) > d {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document pendingTime is too old" )
2019-06-04 23:31:33 +00:00
}
}
2019-04-24 19:12:36 +00:00
payload . document = doc
return & payload , nil
}
2019-07-29 22:54:07 +00:00
2019-10-28 18:50:43 +00:00
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
2023-05-10 06:47:28 +00:00
func ( p * AWS ) AuthorizeSSHSign ( _ context . Context , token string ) ( [ ] SignOption , error ) {
2022-03-10 02:43:45 +00:00
if ! p . ctl . Claimer . IsSSHCAEnabled ( ) {
2021-05-03 19:48:20 +00:00
return nil , errs . Unauthorized ( "aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner '%s'" , p . GetName ( ) )
2019-10-28 18:50:43 +00:00
}
claims , err := p . authorizeToken ( token )
if err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSSHSign" )
2019-10-28 18:50:43 +00:00
}
2019-07-30 00:54:38 +00:00
doc := claims . document
2020-08-03 22:11:42 +00:00
signOptions := [ ] SignOption { }
// Enforce host certificate.
defaults := SignSSHOptions {
CertType : SSHHostCert ,
}
2019-07-30 00:54:38 +00:00
2020-07-30 01:05:35 +00:00
// Validated principals.
principals := [ ] string {
doc . PrivateIP ,
2021-10-08 18:59:57 +00:00
fmt . Sprintf ( "ip-%s.%s.compute.internal" , strings . ReplaceAll ( doc . PrivateIP , "." , "-" ) , doc . Region ) ,
2020-07-30 01:05:35 +00:00
}
2020-03-05 22:33:42 +00:00
// Only enforce known principals if disable custom sans is true.
if p . DisableCustomSANs {
2020-07-30 01:05:35 +00:00
defaults . Principals = principals
2020-08-03 22:11:42 +00:00
} else {
// Check that at least one principal is sent in the request.
signOptions = append ( signOptions , & sshCertOptionsRequireValidator {
Principals : true ,
} )
2020-03-05 22:33:42 +00:00
}
2020-07-30 01:05:35 +00:00
// Certificate templates.
data := sshutil . CreateTemplateData ( sshutil . HostCert , doc . InstanceID , principals )
if v , err := unsafeParseSigned ( token ) ; err == nil {
data . SetToken ( v )
}
2020-08-10 18:26:51 +00:00
templateOptions , err := CustomSSHTemplateOptions ( p . Options , data , sshutil . DefaultIIDTemplate )
2020-07-30 01:05:35 +00:00
if err != nil {
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSSHSign" )
}
signOptions = append ( signOptions , templateOptions )
2019-07-30 00:54:38 +00:00
return append ( signOptions ,
2022-05-19 01:42:42 +00:00
p ,
2020-08-03 22:11:42 +00:00
// Validate user SignSSHOptions.
sshCertOptionsValidator ( defaults ) ,
2019-09-05 01:31:09 +00:00
// Set the validity bounds if not set.
2022-03-10 02:43:45 +00:00
& sshDefaultDuration { p . ctl . Claimer } ,
2019-09-05 01:31:09 +00:00
// Validate public key
2019-09-11 00:04:13 +00:00
& sshDefaultPublicKeyValidator { } ,
2019-09-05 01:31:09 +00:00
// Validate the validity period.
2022-03-10 02:43:45 +00:00
& sshCertValidityValidator { p . ctl . Claimer } ,
2019-09-05 01:31:09 +00:00
// Require all the fields in the SSH certificate
2020-01-24 06:04:34 +00:00
& sshCertDefaultValidator { } ,
2022-01-03 11:25:24 +00:00
// Ensure that all principal names are allowed
2022-05-05 10:32:53 +00:00
newSSHNamePolicyValidator ( p . ctl . getPolicy ( ) . getSSHHost ( ) , nil ) ,
2022-09-30 00:16:26 +00:00
// Call webhooks
2023-07-24 05:29:22 +00:00
p . ctl . newWebhookController (
data ,
linkedca . Webhook_SSH ,
webhook . WithAuthorizationPrincipal ( doc . InstanceID ) ,
) ,
2019-07-30 00:54:38 +00:00
) , nil
2019-07-29 22:54:07 +00:00
}