Skip to content

Commit b1b474d

Browse files
committed
add tests to parseDN (including fuzz tests) and apply changes required to make roundtripping work
Signed-off-by: Tim Ramlot <[email protected]>
1 parent 4ca7b8e commit b1b474d

File tree

4 files changed

+609
-153
lines changed

4 files changed

+609
-153
lines changed

dn.go

+142-68
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package ldap
22

33
import (
4-
"bytes"
54
"encoding/asn1"
65
"encoding/hex"
76
"errors"
87
"fmt"
98
"sort"
109
"strings"
10+
"unicode"
11+
"unicode/utf8"
1112
)
1213

1314
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
@@ -34,6 +35,9 @@ func (a *AttributeTypeAndValue) setValue(s string) error {
3435
// AttributeValue is represented by an number sign ('#' U+0023)
3536
// character followed by the hexadecimal encoding of each of the octets
3637
// of the BER encoding of the X.500 AttributeValue.
38+
//
39+
// WARNING: we only support hex-encoded ASN.1 DER values here, not
40+
// BER encoding. This is a deviation from the RFC.
3741
if len(s) > 0 && s[0] == '#' {
3842
decodedString, err := decodeEncodedString(s[1:])
3943
if err != nil {
@@ -56,59 +60,7 @@ func (a *AttributeTypeAndValue) setValue(s string) error {
5660
// String returns a normalized string representation of this attribute type and
5761
// value pair which is the lowercase join of the Type and Value with a "=".
5862
func (a *AttributeTypeAndValue) String() string {
59-
return strings.ToLower(a.Type) + "=" + a.encodeValue()
60-
}
61-
62-
func (a *AttributeTypeAndValue) encodeValue() string {
63-
// Normalize the value first.
64-
// value := strings.ToLower(a.Value)
65-
value := a.Value
66-
67-
encodedBuf := bytes.Buffer{}
68-
69-
escapeChar := func(c byte) {
70-
encodedBuf.WriteByte('\\')
71-
encodedBuf.WriteByte(c)
72-
}
73-
74-
escapeHex := func(c byte) {
75-
encodedBuf.WriteByte('\\')
76-
encodedBuf.WriteString(hex.EncodeToString([]byte{c}))
77-
}
78-
79-
for i := 0; i < len(value); i++ {
80-
char := value[i]
81-
if i == 0 && char == ' ' || char == '#' {
82-
// Special case leading space or number sign.
83-
escapeChar(char)
84-
continue
85-
}
86-
if i == len(value)-1 && char == ' ' {
87-
// Special case trailing space.
88-
escapeChar(char)
89-
continue
90-
}
91-
92-
switch char {
93-
case '"', '+', ',', ';', '<', '>', '\\':
94-
// Each of these special characters must be escaped.
95-
escapeChar(char)
96-
continue
97-
}
98-
99-
if char < ' ' || char > '~' {
100-
// All special character escapes are handled first
101-
// above. All bytes less than ASCII SPACE and all bytes
102-
// greater than ASCII TILDE must be hex-escaped.
103-
escapeHex(char)
104-
continue
105-
}
106-
107-
// Any other character does not require escaping.
108-
encodedBuf.WriteByte(char)
109-
}
110-
111-
return encodedBuf.String()
63+
return encodeString(foldString(a.Type), false) + "=" + encodeString(a.Value, true)
11264
}
11365

11466
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
@@ -119,12 +71,29 @@ type RelativeDN struct {
11971
// String returns a normalized string representation of this relative DN which
12072
// is the a join of all attributes (sorted in increasing order) with a "+".
12173
func (r *RelativeDN) String() string {
122-
attrs := make([]string, len(r.Attributes))
123-
for i := range r.Attributes {
124-
attrs[i] = r.Attributes[i].String()
74+
builder := strings.Builder{}
75+
sortedAttributes := make([]*AttributeTypeAndValue, len(r.Attributes))
76+
copy(sortedAttributes, r.Attributes)
77+
sortAttributes(sortedAttributes)
78+
for i, atv := range sortedAttributes {
79+
builder.WriteString(atv.String())
80+
if i < len(sortedAttributes)-1 {
81+
builder.WriteByte('+')
82+
}
12583
}
126-
sort.Strings(attrs)
127-
return strings.Join(attrs, "+")
84+
return builder.String()
85+
}
86+
87+
func sortAttributes(atvs []*AttributeTypeAndValue) {
88+
sort.Slice(atvs, func(i, j int) bool {
89+
ti := foldString(atvs[i].Type)
90+
tj := foldString(atvs[j].Type)
91+
if ti != tj {
92+
return ti < tj
93+
}
94+
95+
return atvs[i].Value < atvs[j].Value
96+
})
12897
}
12998

13099
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
@@ -135,23 +104,33 @@ type DN struct {
135104
// String returns a normalized string representation of this DN which is the
136105
// join of all relative DNs with a ",".
137106
func (d *DN) String() string {
138-
rdns := make([]string, len(d.RDNs))
139-
for i := range d.RDNs {
140-
rdns[i] = d.RDNs[i].String()
107+
builder := strings.Builder{}
108+
for i, rdn := range d.RDNs {
109+
builder.WriteString(rdn.String())
110+
if i < len(d.RDNs)-1 {
111+
builder.WriteByte(',')
112+
}
141113
}
142-
return strings.Join(rdns, ",")
114+
return builder.String()
115+
}
116+
117+
func stripLeadingAndTrailingSpaces(inVal string) string {
118+
noSpaces := strings.Trim(inVal, " ")
119+
120+
// Re-add the trailing space if it was an escaped space
121+
if len(noSpaces) > 0 && noSpaces[len(noSpaces)-1] == '\\' && inVal[len(inVal)-1] == ' ' {
122+
noSpaces = noSpaces + " "
123+
}
124+
125+
return noSpaces
143126
}
144127

145128
// Remove leading and trailing spaces from the attribute type and value
146129
// and unescape any escaped characters in these fields
147130
//
148131
// decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170
149132
func decodeString(str string) (string, error) {
150-
s := []rune(strings.TrimSpace(str))
151-
// Re-add the trailing space if the last character was an escaped space character
152-
if len(s) > 0 && s[len(s)-1] == '\\' && str[len(str)-1] == ' ' {
153-
s = append(s, ' ')
154-
}
133+
s := []rune(stripLeadingAndTrailingSpaces(str))
155134

156135
builder := strings.Builder{}
157136
for i := 0; i < len(s); i++ {
@@ -212,6 +191,65 @@ func decodeString(str string) (string, error) {
212191
return builder.String(), nil
213192
}
214193

194+
// Escape a string according to RFC 4514
195+
func encodeString(value string, isValue bool) string {
196+
builder := strings.Builder{}
197+
198+
escapeChar := func(c byte) {
199+
builder.WriteByte('\\')
200+
builder.WriteByte(c)
201+
}
202+
203+
escapeHex := func(c byte) {
204+
builder.WriteByte('\\')
205+
builder.WriteString(hex.EncodeToString([]byte{c}))
206+
}
207+
208+
// Loop through each byte and escape as necessary.
209+
// Runes that take up more than one byte are escaped
210+
// byte by byte (since both bytes are non-ASCII).
211+
for i := 0; i < len(value); i++ {
212+
char := value[i]
213+
if i == 0 && (char == ' ' || char == '#') {
214+
// Special case leading space or number sign.
215+
escapeChar(char)
216+
continue
217+
}
218+
if i == len(value)-1 && char == ' ' {
219+
// Special case trailing space.
220+
escapeChar(char)
221+
continue
222+
}
223+
224+
switch char {
225+
case '"', '+', ',', ';', '<', '>', '\\':
226+
// Each of these special characters must be escaped.
227+
escapeChar(char)
228+
continue
229+
}
230+
231+
if !isValue && char == '=' {
232+
// Equal signs have to be escaped only in the type part of
233+
// the attribute type and value pair.
234+
escapeChar(char)
235+
continue
236+
}
237+
238+
if char < ' ' || char > '~' {
239+
// All special character escapes are handled first
240+
// above. All bytes less than ASCII SPACE and all bytes
241+
// greater than ASCII TILDE must be hex-escaped.
242+
escapeHex(char)
243+
continue
244+
}
245+
246+
// Any other character does not require escaping.
247+
builder.WriteByte(char)
248+
}
249+
250+
return builder.String()
251+
}
252+
215253
func decodeEncodedString(str string) (string, error) {
216254
decoded, err := hex.DecodeString(str)
217255
if err != nil {
@@ -247,12 +285,17 @@ func ParseDN(str string) (*DN, error) {
247285
rdn.Attributes = append(rdn.Attributes, attr)
248286
attr = &AttributeTypeAndValue{}
249287
if end {
288+
sortAttributes(rdn.Attributes)
250289
dn.RDNs = append(dn.RDNs, rdn)
251290
rdn = &RelativeDN{}
252291
}
253292
}
254293
)
255294

295+
// Loop through each character in the string and
296+
// build up the attribute type and value pairs.
297+
// We only check for ascii characters here, which
298+
// allows us to iterate over the string byte by byte.
256299
for i := 0; i < len(str); i++ {
257300
char := str[i]
258301
switch {
@@ -420,3 +463,34 @@ func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool {
420463
func (a *AttributeTypeAndValue) EqualFold(other *AttributeTypeAndValue) bool {
421464
return strings.EqualFold(a.Type, other.Type) && strings.EqualFold(a.Value, other.Value)
422465
}
466+
467+
// foldString returns a folded string such that foldString(x) == foldString(y)
468+
// is identical to bytes.EqualFold(x, y).
469+
// based on https://go.dev/src/encoding/json/fold.go
470+
func foldString(s string) string {
471+
builder := strings.Builder{}
472+
for _, char := range s {
473+
// Handle single-byte ASCII.
474+
if char < utf8.RuneSelf {
475+
if 'A' <= char && char <= 'Z' {
476+
char += 'a' - 'A'
477+
}
478+
builder.WriteRune(char)
479+
continue
480+
}
481+
482+
builder.WriteRune(foldRune(char))
483+
}
484+
return builder.String()
485+
}
486+
487+
// foldRune is returns the smallest rune for all runes in the same fold set.
488+
func foldRune(r rune) rune {
489+
for {
490+
r2 := unicode.SimpleFold(r)
491+
if r2 <= r {
492+
return r
493+
}
494+
r = r2
495+
}
496+
}

0 commit comments

Comments
 (0)