github.com/grailbio/base@v0.0.11/cloud/ec2util/ec2util.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache-2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package ec2util contains a few helper functions related to EC2 (validating
     6  // an Instance Identity Document, extracting a Amazon Resource Name, etc).
     7  //
     8  // Some of the code from this file comes from a Hashicorp Vault
     9  // (covered by Mozilla Public License, version 2.0) file:
    10  // https://github.com/hashicorp/vault/blob/2500218a9cbd833057145aefec1802e6dd5ec8cc/builtin/credential/aws-ec2/path_config_certificate.go
    11  
    12  package ec2util
    13  
    14  import (
    15  	"bytes"
    16  	"crypto/x509"
    17  	"encoding/json"
    18  	"encoding/pem"
    19  	"fmt"
    20  	"regexp"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/aws/aws-sdk-go/service/ec2"
    25  	"v.io/x/lib/vlog"
    26  	"go.mozilla.org/pkcs7"
    27  )
    28  
    29  type IdentityDocument struct {
    30  	InstanceID  string    `json:"instanceId,omitempty"`
    31  	AccountID   string    `json:"accountId,omitempty"`
    32  	Region      string    `json:"region,omitempty"`
    33  	PendingTime time.Time `json:"pendingTime,omitempty"`
    34  }
    35  
    36  var (
    37  	// TODO(razvanm): replace this with a proper parsing of ARNs.
    38  	// Potential source of inspiration: https://github.com/gigawattio/awsarn/blob/master/awsarn.go.
    39  	roleRE                = regexp.MustCompile("^arn:aws:iam::([0-9]*):instance-profile/(.*)$")
    40  	awsPublicCertificates []*x509.Certificate
    41  )
    42  
    43  func init() {
    44  	cert, err := DecodePEMAndParseCertificate(awsPublicCertificatePEM)
    45  	if err != nil {
    46  		panic(err)
    47  	}
    48  	awsPublicCertificates = []*x509.Certificate{cert}
    49  }
    50  
    51  func GetInstance(output *ec2.DescribeInstancesOutput) (*ec2.Instance, error) {
    52  	if len(output.Reservations) != 1 {
    53  		return nil, fmt.Errorf("unexpected number of Reservations (want 1): %+v", output)
    54  	}
    55  
    56  	reservation := output.Reservations[0]
    57  	if len(reservation.Instances) != 1 {
    58  		return nil, fmt.Errorf("unexpected number of Instances (want 1): %+v", output)
    59  	}
    60  
    61  	instance := reservation.Instances[0]
    62  	if instance.IamInstanceProfile == nil {
    63  		return nil, fmt.Errorf("non-nil IamInstanceProfile is required: %+v", output)
    64  	}
    65  
    66  	return instance, nil
    67  }
    68  
    69  // GetIamInstanceProfileARN extracts the ARN from the `instance` output of a call to
    70  // DescribeInstances. The ARN is expected to be non-empty.
    71  func GetIamInstanceProfileARN(instance *ec2.Instance) (string, error) {
    72  	if instance == nil {
    73  		return "", fmt.Errorf("non-nil instance is required: %+v", instance)
    74  	}
    75  
    76  	if instance.IamInstanceProfile == nil {
    77  		return "", fmt.Errorf("non-nil IamInstanceProfile is required: %+v", instance)
    78  	}
    79  
    80  	profile := instance.IamInstanceProfile
    81  	if profile.Arn == nil {
    82  		return "", fmt.Errorf("non-nil Arn is required: %+v", instance)
    83  	}
    84  
    85  	if len(*profile.Arn) == 0 {
    86  		return "", fmt.Errorf("non-empty Arn is required: %+v", instance)
    87  	}
    88  
    89  	return *profile.Arn, nil
    90  }
    91  
    92  // GetPublicIPAddress extracts the public IP address from the output of a call
    93  // to DescribeInstances Instance. The response is expected to be non-empty if the
    94  // instance has a public IP and empty ("") if the instance is private.
    95  func GetPublicIPAddress(instance *ec2.Instance) (string, error) {
    96  	if instance == nil {
    97  		return "", fmt.Errorf("non-nil instance is required: %+v", instance)
    98  	}
    99  
   100  	if instance.PublicIpAddress == nil || len(*instance.PublicIpAddress) == 0 {
   101  		return "", nil
   102  	}
   103  
   104  	return *instance.PublicIpAddress, nil
   105  }
   106  
   107  // GetPrivateIPAddress extracts the private IP address from the output of a call
   108  // to DescribeInstances Instance. The response is expected to be the first private IP
   109  // attached to the instance.
   110  // If the instances no attached interfaces, the value is empty ("")
   111  func GetPrivateIPAddress(instance *ec2.Instance) (string, error) {
   112  	if instance == nil {
   113  		return "", fmt.Errorf("non-nil instance is required: %+v", instance)
   114  	}
   115  
   116  	if instance.PrivateIpAddress == nil || len(*instance.PrivateIpAddress) == 0 {
   117  		return "", nil
   118  	}
   119  
   120  	return *instance.PrivateIpAddress, nil
   121  }
   122  
   123  // GetTags returns a map of Key/Value pairs representing the tags
   124  func GetTags(instance *ec2.Instance) ([]*ec2.Tag, error) {
   125  	if instance == nil {
   126  		return nil, fmt.Errorf("non-nil instance is required: %+v", instance)
   127  	}
   128  
   129  	if instance.Tags == nil || len(instance.Tags) == 0 {
   130  		return nil, nil
   131  	}
   132  
   133  	return instance.Tags, nil
   134  }
   135  
   136  // GetInstanceId returns the instanceID from the output of a call
   137  // to DescribeInstances Instance.
   138  func GetInstanceId(instance *ec2.Instance) (string, error) {
   139  	if instance == nil {
   140  		return "", fmt.Errorf("non-nil instance is required: %+v", instance)
   141  	}
   142  
   143  	if instance.InstanceId == nil || len(*instance.InstanceId) == 0 {
   144  		return "", nil
   145  	}
   146  
   147  	return *instance.InstanceId, nil
   148  }
   149  
   150  // ValidateInstance checks if an EC2 instance exists and it has the expected
   151  // IP. It returns the name of the instance profile (the IAM role).
   152  //
   153  // Note that this validation will not work for NATed VMs.
   154  func ValidateInstance(output *ec2.DescribeInstancesOutput, doc IdentityDocument, remoteAddr string) (role string, err error) {
   155  	vlog.Infof("reservations:\n%+v", output.Reservations)
   156  
   157  	instance, err := GetInstance(output)
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  
   162  	publicIP, err := GetPublicIPAddress(instance)
   163  	if err != nil {
   164  		return "", err
   165  	}
   166  
   167  	// Instances that do not have a public IP should be able to authenticate
   168  	// with ticket server. Connections from such instances are routed through a
   169  	// NAT gateway with an Elastic IP. The following check which ensures the
   170  	// remoteAddr from which the connection originates is same as the public IP
   171  	// of the instance is skipped for private instances.
   172  	if remoteAddr != "" && publicIP != "" {
   173  		if !strings.HasPrefix(remoteAddr, publicIP+":") {
   174  			return "", fmt.Errorf("mismatch between the real peer address (%s) and public IP of the instance (%s)", remoteAddr, publicIP)
   175  		}
   176  	}
   177  
   178  	arn, err := GetIamInstanceProfileARN(instance)
   179  	if err != nil {
   180  		return "", err
   181  	}
   182  	m := roleRE.FindStringSubmatch(arn)
   183  	if len(m) != 3 {
   184  		return "", fmt.Errorf("unexpected ARN format for %q", arn)
   185  	}
   186  	vlog.Infof("IAM role: %q parsed: %q", arn, m)
   187  
   188  	accountID, role := m[1], m[2]
   189  
   190  	if accountID != doc.AccountID {
   191  		return "", fmt.Errorf("mismatch between account ID in Identity Doc (%q) and role (%q): %q", doc.AccountID, accountID, arn)
   192  	}
   193  	return role, nil
   194  }
   195  
   196  // ParseAndVerifyIdentityDocument parses and checks and identity document in
   197  // PKCS#7 format. Only some relevant fields are returned.
   198  func ParseAndVerifyIdentityDocument(pkcs7b64 string) (*IdentityDocument, string, error) {
   199  	// Insert the header and footer for the signature to be able to pem decode it.
   200  	s := fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", pkcs7b64)
   201  
   202  	// Decode the PEM encoded signature.
   203  	pkcs7BER, pkcs7Rest := pem.Decode([]byte(s))
   204  	if len(pkcs7Rest) != 0 {
   205  		return nil, "", fmt.Errorf("failed to decode the PKCS#7 signature")
   206  	}
   207  
   208  	// Parse the signature from asn1 format into a struct.
   209  	pkcs7Data, err := pkcs7.Parse(pkcs7BER.Bytes)
   210  	if err != nil {
   211  		return nil, "", fmt.Errorf("failed to parse the BER encoded PKCS#7 signature: %s\n", err)
   212  	}
   213  
   214  	pkcs7Data.Certificates = awsPublicCertificates
   215  
   216  	// Verify extracts the authenticated attributes in the PKCS#7
   217  	// signature, and verifies the authenticity of the content using
   218  	// 'dsa.PublicKey' embedded in the public certificate.
   219  	if err := pkcs7Data.Verify(); err != nil {
   220  		return nil, "", fmt.Errorf("failed to verify the signature: %v", err)
   221  	}
   222  
   223  	// Check if the signature has content inside of it.
   224  	if len(pkcs7Data.Content) == 0 {
   225  		return nil, "", fmt.Errorf("instance identity document could not be found in the signature")
   226  	}
   227  
   228  	var identityDoc IdentityDocument
   229  	content := string(pkcs7Data.Content)
   230  	vlog.VI(1).Infof("%v", content)
   231  	decoder := json.NewDecoder(bytes.NewReader(pkcs7Data.Content))
   232  	decoder.UseNumber()
   233  	if err := decoder.Decode(&identityDoc); err != nil {
   234  		return nil, "", err
   235  	}
   236  
   237  	return &identityDoc, content, nil
   238  }
   239  
   240  // DecodePEMAndParseCertificate decodes the PEM encoded certificate and
   241  // parses it into a x509 cert.
   242  func DecodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) {
   243  	// Decode the PEM block and error out if a block is not detected in
   244  	// the first attempt.
   245  	decodedPublicCert, rest := pem.Decode([]byte(certificate))
   246  	if len(rest) != 0 {
   247  		return nil, fmt.Errorf("invalid certificate; should be one PEM block only")
   248  	}
   249  
   250  	// Check if the certificate can be parsed.
   251  	publicCert, err := x509.ParseCertificate(decodedPublicCert.Bytes)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	if publicCert == nil {
   256  		return nil, fmt.Errorf("invalid certificate; failed to parse certificate")
   257  	}
   258  	return publicCert, nil
   259  }