summaryrefslogtreecommitdiffstats
path: root/pkg/cryptoutils/sans.go
blob: d237ef58ea64e26fa872f4e4eaed2c02e853a800 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cryptoutils

import (
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"errors"
	"fmt"
)

var (
	// OIDOtherName is the OID for the OtherName SAN per RFC 5280
	OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7}
	// SANOID is the OID for Subject Alternative Name per RFC 5280
	SANOID = asn1.ObjectIdentifier{2, 5, 29, 17}
)

// OtherName describes a name related to a certificate which is not in one
// of the standard name formats. RFC 5280, 4.2.1.6:
//
//	OtherName ::= SEQUENCE {
//	     type-id    OBJECT IDENTIFIER,
//	     value      [0] EXPLICIT ANY DEFINED BY type-id }
//
// OtherName for Fulcio-issued certificates only supports UTF-8 strings as values.
type OtherName struct {
	ID    asn1.ObjectIdentifier
	Value string `asn1:"utf8,explicit,tag:0"`
}

// MarshalOtherNameSAN creates a Subject Alternative Name extension
// with an OtherName sequence. RFC 5280, 4.2.1.6:
//
// SubjectAltName ::= GeneralNames
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
// GeneralName ::= CHOICE {
//
//	otherName                       [0]     OtherName,
//	... }
func MarshalOtherNameSAN(name string, critical bool) (*pkix.Extension, error) {
	o := OtherName{
		ID:    OIDOtherName,
		Value: name,
	}
	bytes, err := asn1.MarshalWithParams(o, "tag:0")
	if err != nil {
		return nil, err
	}

	sans, err := asn1.Marshal([]asn1.RawValue{{FullBytes: bytes}})
	if err != nil {
		return nil, err
	}
	return &pkix.Extension{
		Id:       SANOID,
		Critical: critical,
		Value:    sans,
	}, nil
}

// UnmarshalOtherNameSAN extracts a UTF-8 string from the OtherName
// field in the Subject Alternative Name extension.
func UnmarshalOtherNameSAN(exts []pkix.Extension) (string, error) {
	var otherNames []string

	for _, e := range exts {
		if !e.Id.Equal(SANOID) {
			continue
		}

		var seq asn1.RawValue
		rest, err := asn1.Unmarshal(e.Value, &seq)
		if err != nil {
			return "", err
		} else if len(rest) != 0 {
			return "", fmt.Errorf("trailing data after X.509 extension")
		}
		if !seq.IsCompound || seq.Tag != asn1.TagSequence || seq.Class != asn1.ClassUniversal {
			return "", asn1.StructuralError{Msg: "bad SAN sequence"}
		}

		rest = seq.Bytes
		for len(rest) > 0 {
			var v asn1.RawValue
			rest, err = asn1.Unmarshal(rest, &v)
			if err != nil {
				return "", err
			}

			// skip all GeneralName fields except OtherName
			if v.Tag != 0 {
				continue
			}

			var other OtherName
			if _, err := asn1.UnmarshalWithParams(v.FullBytes, &other, "tag:0"); err != nil {
				return "", fmt.Errorf("could not parse requested OtherName SAN: %w", err)
			}
			if !other.ID.Equal(OIDOtherName) {
				return "", fmt.Errorf("unexpected OID for OtherName, expected %v, got %v", OIDOtherName, other.ID)
			}
			otherNames = append(otherNames, other.Value)
		}
	}

	if len(otherNames) == 0 {
		return "", errors.New("no OtherName found")
	}
	if len(otherNames) != 1 {
		return "", errors.New("expected only one OtherName")
	}

	return otherNames[0], nil
}

// GetSubjectAlternateNames extracts all subject alternative names from
// the certificate, including email addresses, DNS, IP addresses, URIs,
// and OtherName SANs
func GetSubjectAlternateNames(cert *x509.Certificate) []string {
	sans := []string{}
	sans = append(sans, cert.DNSNames...)
	sans = append(sans, cert.EmailAddresses...)
	for _, ip := range cert.IPAddresses {
		sans = append(sans, ip.String())
	}
	for _, uri := range cert.URIs {
		sans = append(sans, uri.String())
	}
	// ignore error if there's no OtherName SAN
	otherName, _ := UnmarshalOtherNameSAN(cert.Extensions)
	if len(otherName) > 0 {
		sans = append(sans, otherName)
	}
	return sans
}