pkg/controller/common/certificates/x509_othername.go (134 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package certificates
import (
"crypto/x509"
"encoding/asn1"
"errors"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
)
var (
// SubjectAlternativeNamesObjectIdentifier is the OID for the Subject Alternative Name x509 extension
SubjectAlternativeNamesObjectIdentifier = asn1.ObjectIdentifier{2, 5, 29, 17}
// CommonNameObjectIdentifier is the OID for a CommonName field in x509
CommonNameObjectIdentifier = asn1.ObjectIdentifier{2, 5, 4, 3}
)
/*
GeneralName is a partially modelled GeneralName from RFC 5280, Section 4.2.1.6
The RFC defines the Subject Alternative Names value as follows:
id-ce-subjectAltName OBJECT IDENTIFIER ::= { id-ce 17 }
SubjectAltName ::= GeneralNames
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
GeneralName ::= CHOICE {
otherName [0] OtherName,
rfc822Name [1] IA5String,
dNSName [2] IA5String,
x400Address [3] ORAddress,
directoryName [4] Name,
ediPartyName [5] EDIPartyName,
uniformResourceIdentifier [6] IA5String,
iPAddress [7] OCTET STRING,
registeredID [8] OBJECT IDENTIFIER }
OtherName ::= SEQUENCE {
type-id OBJECT IDENTIFIER,
value [0] EXPLICIT ANY DEFINED BY type-id }
OtherName is used in Elasticsearch certificates as the node names, and is what is compared to the allowed subjects
in the trust_restrictions file (if configured) when doing certificate validation between ES nodes.
We only model OtherName, DNSName and IPAddress here because those are what we use for the Elasticsearch certs
*/
type GeneralName struct {
OtherName OtherName `asn1:"optional,tag:0"`
DNSName string `asn1:"optional,ia5,tag:2"`
IPAddress []byte `asn1:"optional,tag:7"`
}
// OtherName is a record that contains custom data. The OID defines how the Value should be parsed.
type OtherName struct {
OID asn1.ObjectIdentifier
Value asn1.RawValue
}
// ToUTF8StringValuedOtherName converts the OtherName instance into an UTF8StringValuedOtherName
func (n *OtherName) ToUTF8StringValuedOtherName() (*UTF8StringValuedOtherName, error) {
var utf8StringValuedOtherName UTF8StringValuedOtherName
if err := convertASN1(*n, &utf8StringValuedOtherName); err != nil {
return nil, err
}
return &utf8StringValuedOtherName, nil
}
// UTF8StringValuedOtherName is a concrete OtherValue where the Value is a utf8 string.
type UTF8StringValuedOtherName struct {
OID asn1.ObjectIdentifier
Value string `asn1:"utf8,explicit"` // like openssl
}
// ToOtherName converts the UTF8StringValuedOtherName instance into an OtherName
func (n *UTF8StringValuedOtherName) ToOtherName() (*OtherName, error) {
var otherName OtherName
if err := convertASN1(*n, &otherName); err != nil {
return nil, err
}
return &otherName, nil
}
// convertASN1 converts a struct to another through asn1 marshalling and unmarshalling
func convertASN1(from, to interface{}) error {
data, err := asn1.Marshal(from)
if err != nil {
return err
}
if rest, err := asn1.Unmarshal(data, to); err != nil {
return err
} else if len(rest) != 0 {
return asn1.StructuralError{Msg: "trailing data after unmarshalling"}
}
return nil
}
// MarshalToSubjectAlternativeNamesData marshals the provided General Names to a valid value for an X509 SAN extension
func MarshalToSubjectAlternativeNamesData(generalNames []GeneralName) ([]byte, error) {
sanData, err := asn1.Marshal(generalNames)
if err != nil {
return nil, err
}
// somehow go wraps each entry as its own sequence, so we need to unwrap each entry to produce a valid SAN
return flattenNestedASN1Sequence(sanData)
}
// flattenNestedASN1Sequence flattens one level of nested sequences in asn1-encoded data:
// e.g: [[1], [2], [3]] -> [1,2,3]
func flattenNestedASN1Sequence(b []byte) ([]byte, error) {
var value asn1.RawValue
rest, err := asn1.Unmarshal(b, &value)
if err != nil {
return nil, err
}
if len(rest) != 0 {
return nil, errors.New("trailing asn1 data")
}
var unwrappedBytes []byte
rest = value.Bytes
for len(rest) > 0 {
var nestedValue asn1.RawValue
rest, err = asn1.Unmarshal(rest, &nestedValue)
if err != nil {
return nil, err
}
unwrappedBytes = append(unwrappedBytes, nestedValue.Bytes...)
}
value.Bytes = unwrappedBytes
value.FullBytes = nil
b, err = asn1.Marshal(value)
return b, err
}
// ParseSANGeneralNamesOtherNamesOnly parses the X509 Subject Alternative Names extensions of a X509 certificate and
// returns a list of GeneralName entries.
//
// Note: Only OtherName entries are returned, any other entry is ignored.
func ParseSANGeneralNamesOtherNamesOnly(c *x509.Certificate) ([]GeneralName, error) {
var generalNames []GeneralName
for _, ext := range c.Extensions {
//nolint:nestif
if SubjectAlternativeNamesObjectIdentifier.Equal(ext.Id) {
// rfc: should be wrapped in a sequence node:
var generalNamesValue asn1.RawValue
rest, err := asn1.Unmarshal(ext.Value, &generalNamesValue)
if err != nil {
return nil, err
}
if len(rest) != 0 {
return nil, errors.New("trailing data after SubjectAlternativeNames")
}
if generalNamesValue.Class != asn1.ClassUniversal || generalNamesValue.Tag != asn1.TagSequence {
return nil, errors.New("invalid GeneralNames class or tag")
}
rest = generalNamesValue.Bytes
for len(rest) != 0 {
var generalName asn1.RawValue
rest, err = asn1.Unmarshal(rest, &generalName)
if err != nil {
return nil, err
}
if generalName.Class == asn1.ClassContextSpecific {
switch generalName.Tag {
case 0:
// OtherName ::= SEQUENCE {
// type-id OBJECT IDENTIFIER,
// value [0] EXPLICIT ANY DEFINED BY type-id }
var otherNameTypeObjectIdentifier asn1.ObjectIdentifier
otherNameValueBytes, err := asn1.Unmarshal(generalName.Bytes, &otherNameTypeObjectIdentifier)
if err != nil {
return nil, err
}
var value asn1.RawValue
vrest, err := asn1.Unmarshal(otherNameValueBytes, &value)
if err != nil {
return nil, err
}
if len(vrest) != 0 {
return nil, errors.New("trailing data after OtherName value")
}
generalNames = append(generalNames, GeneralName{
OtherName: OtherName{
OID: otherNameTypeObjectIdentifier,
Value: value,
},
})
default:
// only used in tests
ulog.Log.Info("Ignoring unsupported GeneralNames tag", "tag", generalName.Tag, "subject", c.Subject)
}
}
}
}
}
return generalNames, nil
}