istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/util/san.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package util
    16  
    17  import (
    18  	"crypto/x509/pkix"
    19  	"encoding/asn1"
    20  	"fmt"
    21  	"net/netip"
    22  	"strings"
    23  
    24  	"istio.io/istio/pkg/spiffe"
    25  )
    26  
    27  // IdentityType represents type of an identity. This is used to properly encode
    28  // an identity into a SAN extension.
    29  type IdentityType int
    30  
    31  const (
    32  	// TypeDNS represents a DNS name.
    33  	TypeDNS IdentityType = iota
    34  	// TypeIP represents an IP address.
    35  	TypeIP
    36  	// TypeURI represents a universal resource identifier.
    37  	TypeURI
    38  )
    39  
    40  var (
    41  	// Mapping from the type of an identity to the OID tag value for the X.509
    42  	// SAN field (see https://tools.ietf.org/html/rfc5280#appendix-A.2)
    43  	//
    44  	// SubjectAltName ::= GeneralNames
    45  	//
    46  	// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
    47  	//
    48  	// GeneralName ::= CHOICE {
    49  	//      dNSName                         [2]     IA5String,
    50  	//      uniformResourceIdentifier       [6]     IA5String,
    51  	//      iPAddress                       [7]     OCTET STRING,
    52  	// }
    53  	oidTagMap = map[IdentityType]int{
    54  		TypeDNS: 2,
    55  		TypeURI: 6,
    56  		TypeIP:  7,
    57  	}
    58  
    59  	// A reversed map that maps from an OID tag to the corresponding identity
    60  	// type.
    61  	identityTypeMap = generateReversedMap(oidTagMap)
    62  
    63  	// The OID for the SAN extension (See
    64  	// http://www.alvestrand.no/objectid/2.5.29.17.html).
    65  	oidSubjectAlternativeName = asn1.ObjectIdentifier{2, 5, 29, 17}
    66  )
    67  
    68  // Identity is an object holding both the encoded identifier bytes as well as
    69  // the type of the identity.
    70  type Identity struct {
    71  	Type  IdentityType
    72  	Value []byte
    73  }
    74  
    75  // BuildSubjectAltNameExtension builds the SAN extension for the certificate.
    76  func BuildSubjectAltNameExtension(hosts string) (*pkix.Extension, error) {
    77  	ids := []Identity{}
    78  	for _, host := range strings.Split(hosts, ",") {
    79  		if ipa, _ := netip.ParseAddr(host); ipa.IsValid() {
    80  			// Use the 4-byte representation of the IP address when possible.
    81  			ip := ipa.AsSlice()
    82  			if ipa.Is4In6() {
    83  				eip := ipa.As4()
    84  				ip = eip[:]
    85  			}
    86  			ids = append(ids, Identity{Type: TypeIP, Value: ip})
    87  		} else if strings.HasPrefix(host, spiffe.URIPrefix) {
    88  			ids = append(ids, Identity{Type: TypeURI, Value: []byte(host)})
    89  		} else {
    90  			ids = append(ids, Identity{Type: TypeDNS, Value: []byte(host)})
    91  		}
    92  	}
    93  
    94  	san, err := BuildSANExtension(ids)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("SAN extension building failure (%v)", err)
    97  	}
    98  
    99  	return san, nil
   100  }
   101  
   102  // BuildSANExtension builds a `pkix.Extension` of type "Subject
   103  // Alternative Name" based on the given identities.
   104  func BuildSANExtension(identites []Identity) (*pkix.Extension, error) {
   105  	rawValues := []asn1.RawValue{}
   106  	for _, i := range identites {
   107  		tag, ok := oidTagMap[i.Type]
   108  		if !ok {
   109  			return nil, fmt.Errorf("unsupported identity type: %v", i.Type)
   110  		}
   111  
   112  		rawValues = append(rawValues, asn1.RawValue{
   113  			Bytes: i.Value,
   114  			Class: asn1.ClassContextSpecific,
   115  			Tag:   tag,
   116  		})
   117  	}
   118  
   119  	bs, err := asn1.Marshal(rawValues)
   120  	if err != nil {
   121  		return nil, fmt.Errorf("failed to marshal the raw values for SAN field (err: %s)", err)
   122  	}
   123  
   124  	// SAN is Critical because the subject is empty. This is compliant with X.509 and SPIFFE standards.
   125  	return &pkix.Extension{Id: oidSubjectAlternativeName, Critical: true, Value: bs}, nil
   126  }
   127  
   128  // ExtractIDsFromSAN takes a SAN extension and extracts the identities.
   129  // The logic is mostly borrowed from
   130  // https://github.com/golang/go/blob/master/src/crypto/x509/x509.go, with the
   131  // addition of supporting extracting URIs.
   132  func ExtractIDsFromSAN(sanExt *pkix.Extension) ([]Identity, error) {
   133  	if !sanExt.Id.Equal(oidSubjectAlternativeName) {
   134  		return nil, fmt.Errorf("the input is not a SAN extension")
   135  	}
   136  
   137  	var sequence asn1.RawValue
   138  	if rest, err := asn1.Unmarshal(sanExt.Value, &sequence); err != nil {
   139  		return nil, err
   140  	} else if len(rest) != 0 {
   141  		return nil, fmt.Errorf("the SAN extension is incorrectly encoded")
   142  	}
   143  
   144  	// Check the rawValue is a sequence.
   145  	if !sequence.IsCompound || sequence.Tag != asn1.TagSequence || sequence.Class != asn1.ClassUniversal {
   146  		return nil, fmt.Errorf("the SAN extension is incorrectly encoded")
   147  	}
   148  
   149  	ids := []Identity{}
   150  	for bytes := sequence.Bytes; len(bytes) > 0; {
   151  		var rawValue asn1.RawValue
   152  		var err error
   153  
   154  		bytes, err = asn1.Unmarshal(bytes, &rawValue)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  		ids = append(ids, Identity{Type: identityTypeMap[rawValue.Tag], Value: rawValue.Bytes})
   159  	}
   160  
   161  	return ids, nil
   162  }
   163  
   164  // ExtractSANExtension extracts the "Subject Alternative Name" externsion from
   165  // the given PKIX extension set.
   166  func ExtractSANExtension(exts []pkix.Extension) *pkix.Extension {
   167  	for _, ext := range exts {
   168  		if ext.Id.Equal(oidSubjectAlternativeName) {
   169  			// We don't need to examine other extensions anymore since a certificate
   170  			// must not include more than one instance of a particular extension. See
   171  			// https://tools.ietf.org/html/rfc5280#section-4.2.
   172  			return &ext
   173  		}
   174  	}
   175  	return nil
   176  }
   177  
   178  // ExtractIDs first finds the SAN extension from the given extension set, then
   179  // extract identities from the SAN extension.
   180  func ExtractIDs(exts []pkix.Extension) ([]string, error) {
   181  	sanExt := ExtractSANExtension(exts)
   182  	if sanExt == nil {
   183  		return nil, fmt.Errorf("the SAN extension does not exist")
   184  	}
   185  
   186  	idsWithType, err := ExtractIDsFromSAN(sanExt)
   187  	if err != nil {
   188  		return nil, fmt.Errorf("failed to extract identities from SAN extension (error %v)", err)
   189  	}
   190  
   191  	ids := []string{}
   192  	for _, id := range idsWithType {
   193  		ids = append(ids, string(id.Value))
   194  	}
   195  	return ids, nil
   196  }
   197  
   198  func generateReversedMap(m map[IdentityType]int) map[int]IdentityType {
   199  	reversed := make(map[int]IdentityType)
   200  	for key, value := range m {
   201  		reversed[value] = key
   202  	}
   203  	return reversed
   204  }