Skip to content

Commit 9c14185

Browse files
authored
feat: Add tests to parseDN (fuzzing, roundtrip ...) (#504)
* add tests to parseDN (including fuzz tests) and apply changes required to make roundtripping work Signed-off-by: Tim Ramlot <[email protected]> * undo unnessary changes Signed-off-by: Tim Ramlot <[email protected]> --------- Signed-off-by: Tim Ramlot <[email protected]>
1 parent 4ca7b8e commit 9c14185

File tree

5 files changed

+544
-135
lines changed

5 files changed

+544
-135
lines changed

dn.go

+112-59
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
@@ -142,16 +94,23 @@ func (d *DN) String() string {
14294
return strings.Join(rdns, ",")
14395
}
14496

97+
func stripLeadingAndTrailingSpaces(inVal string) string {
98+
noSpaces := strings.Trim(inVal, " ")
99+
100+
// Re-add the trailing space if it was an escaped space
101+
if len(noSpaces) > 0 && noSpaces[len(noSpaces)-1] == '\\' && inVal[len(inVal)-1] == ' ' {
102+
noSpaces = noSpaces + " "
103+
}
104+
105+
return noSpaces
106+
}
107+
145108
// Remove leading and trailing spaces from the attribute type and value
146109
// and unescape any escaped characters in these fields
147110
//
148111
// decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170
149112
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-
}
113+
s := []rune(stripLeadingAndTrailingSpaces(str))
155114

156115
builder := strings.Builder{}
157116
for i := 0; i < len(s); i++ {
@@ -212,6 +171,65 @@ func decodeString(str string) (string, error) {
212171
return builder.String(), nil
213172
}
214173

174+
// Escape a string according to RFC 4514
175+
func encodeString(value string, isValue bool) string {
176+
builder := strings.Builder{}
177+
178+
escapeChar := func(c byte) {
179+
builder.WriteByte('\\')
180+
builder.WriteByte(c)
181+
}
182+
183+
escapeHex := func(c byte) {
184+
builder.WriteByte('\\')
185+
builder.WriteString(hex.EncodeToString([]byte{c}))
186+
}
187+
188+
// Loop through each byte and escape as necessary.
189+
// Runes that take up more than one byte are escaped
190+
// byte by byte (since both bytes are non-ASCII).
191+
for i := 0; i < len(value); i++ {
192+
char := value[i]
193+
if i == 0 && (char == ' ' || char == '#') {
194+
// Special case leading space or number sign.
195+
escapeChar(char)
196+
continue
197+
}
198+
if i == len(value)-1 && char == ' ' {
199+
// Special case trailing space.
200+
escapeChar(char)
201+
continue
202+
}
203+
204+
switch char {
205+
case '"', '+', ',', ';', '<', '>', '\\':
206+
// Each of these special characters must be escaped.
207+
escapeChar(char)
208+
continue
209+
}
210+
211+
if !isValue && char == '=' {
212+
// Equal signs have to be escaped only in the type part of
213+
// the attribute type and value pair.
214+
escapeChar(char)
215+
continue
216+
}
217+
218+
if char < ' ' || char > '~' {
219+
// All special character escapes are handled first
220+
// above. All bytes less than ASCII SPACE and all bytes
221+
// greater than ASCII TILDE must be hex-escaped.
222+
escapeHex(char)
223+
continue
224+
}
225+
226+
// Any other character does not require escaping.
227+
builder.WriteByte(char)
228+
}
229+
230+
return builder.String()
231+
}
232+
215233
func decodeEncodedString(str string) (string, error) {
216234
decoded, err := hex.DecodeString(str)
217235
if err != nil {
@@ -253,6 +271,10 @@ func ParseDN(str string) (*DN, error) {
253271
}
254272
)
255273

274+
// Loop through each character in the string and
275+
// build up the attribute type and value pairs.
276+
// We only check for ascii characters here, which
277+
// allows us to iterate over the string byte by byte.
256278
for i := 0; i < len(str); i++ {
257279
char := str[i]
258280
switch {
@@ -420,3 +442,34 @@ func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool {
420442
func (a *AttributeTypeAndValue) EqualFold(other *AttributeTypeAndValue) bool {
421443
return strings.EqualFold(a.Type, other.Type) && strings.EqualFold(a.Value, other.Value)
422444
}
445+
446+
// foldString returns a folded string such that foldString(x) == foldString(y)
447+
// is identical to bytes.EqualFold(x, y).
448+
// based on https://go.dev/src/encoding/json/fold.go
449+
func foldString(s string) string {
450+
builder := strings.Builder{}
451+
for _, char := range s {
452+
// Handle single-byte ASCII.
453+
if char < utf8.RuneSelf {
454+
if 'A' <= char && char <= 'Z' {
455+
char += 'a' - 'A'
456+
}
457+
builder.WriteRune(char)
458+
continue
459+
}
460+
461+
builder.WriteRune(foldRune(char))
462+
}
463+
return builder.String()
464+
}
465+
466+
// foldRune is returns the smallest rune for all runes in the same fold set.
467+
func foldRune(r rune) rune {
468+
for {
469+
r2 := unicode.SimpleFold(r)
470+
if r2 <= r {
471+
return r
472+
}
473+
r = r2
474+
}
475+
}

0 commit comments

Comments
 (0)