1
1
package ldap
2
2
3
3
import (
4
- "bytes"
5
4
"encoding/asn1"
6
5
"encoding/hex"
7
6
"errors"
8
7
"fmt"
9
8
"sort"
10
9
"strings"
10
+ "unicode"
11
+ "unicode/utf8"
11
12
)
12
13
13
14
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
@@ -34,6 +35,9 @@ func (a *AttributeTypeAndValue) setValue(s string) error {
34
35
// AttributeValue is represented by an number sign ('#' U+0023)
35
36
// character followed by the hexadecimal encoding of each of the octets
36
37
// 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.
37
41
if len (s ) > 0 && s [0 ] == '#' {
38
42
decodedString , err := decodeEncodedString (s [1 :])
39
43
if err != nil {
@@ -56,59 +60,7 @@ func (a *AttributeTypeAndValue) setValue(s string) error {
56
60
// String returns a normalized string representation of this attribute type and
57
61
// value pair which is the lowercase join of the Type and Value with a "=".
58
62
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 )
112
64
}
113
65
114
66
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
@@ -119,12 +71,29 @@ type RelativeDN struct {
119
71
// String returns a normalized string representation of this relative DN which
120
72
// is the a join of all attributes (sorted in increasing order) with a "+".
121
73
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
+ }
125
83
}
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
+ })
128
97
}
129
98
130
99
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
@@ -135,23 +104,33 @@ type DN struct {
135
104
// String returns a normalized string representation of this DN which is the
136
105
// join of all relative DNs with a ",".
137
106
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
+ }
141
113
}
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
143
126
}
144
127
145
128
// Remove leading and trailing spaces from the attribute type and value
146
129
// and unescape any escaped characters in these fields
147
130
//
148
131
// decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170
149
132
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 ))
155
134
156
135
builder := strings.Builder {}
157
136
for i := 0 ; i < len (s ); i ++ {
@@ -212,6 +191,65 @@ func decodeString(str string) (string, error) {
212
191
return builder .String (), nil
213
192
}
214
193
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
+
215
253
func decodeEncodedString (str string ) (string , error ) {
216
254
decoded , err := hex .DecodeString (str )
217
255
if err != nil {
@@ -247,12 +285,17 @@ func ParseDN(str string) (*DN, error) {
247
285
rdn .Attributes = append (rdn .Attributes , attr )
248
286
attr = & AttributeTypeAndValue {}
249
287
if end {
288
+ sortAttributes (rdn .Attributes )
250
289
dn .RDNs = append (dn .RDNs , rdn )
251
290
rdn = & RelativeDN {}
252
291
}
253
292
}
254
293
)
255
294
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.
256
299
for i := 0 ; i < len (str ); i ++ {
257
300
char := str [i ]
258
301
switch {
@@ -420,3 +463,34 @@ func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool {
420
463
func (a * AttributeTypeAndValue ) EqualFold (other * AttributeTypeAndValue ) bool {
421
464
return strings .EqualFold (a .Type , other .Type ) && strings .EqualFold (a .Value , other .Value )
422
465
}
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