From c4ad4d4beca2bafff6c1592071328b9ea2ddab1b Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Fri, 22 Jul 2022 11:31:12 +0200 Subject: [PATCH] Add String() methods to DN and its subtypes (#386) * Add String() methods to DN and its subtypes This patch adds `String() string` methods to each of the following types: - DN - RelativeDN - AttributeTypeAndValue So that a `*DN` implements the `fmt.Stringer` interface. These methods also produce normalized strings: Attribute Type and Value are lowercased and joined with a "=" character while multiple attributes of a Relative DN are sorted lexicographically before being joined witha "+" character. This allows one to use the string representation of a DN as a map key and ensure that two DNs which `Equal()` eachother would have the same `String()` value. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) * Mirror DN String() methods to v3 folder Co-authored-by: Josh Hawn --- dn.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ dn_test.go | 13 +++++--- v3/dn.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++- v3/dn_test.go | 13 +++++--- 4 files changed, 179 insertions(+), 9 deletions(-) diff --git a/dn.go b/dn.go index 916984b9..adbdb786 100644 --- a/dn.go +++ b/dn.go @@ -5,6 +5,7 @@ import ( enchex "encoding/hex" "errors" "fmt" + "sort" "strings" ber "github.com/go-asn1-ber/asn1-ber" @@ -18,16 +19,95 @@ type AttributeTypeAndValue struct { Value string } +// String returns a normalized string representation of this attribute type and +// value pair which is the a lowercased join of the Type and Value with a "=". +func (a *AttributeTypeAndValue) String() string { + return strings.ToLower(a.Type) + "=" + a.encodeValue() +} + +func (a *AttributeTypeAndValue) encodeValue() string { + // Normalize the value first. + // value := strings.ToLower(a.Value) + value := a.Value + + encodedBuf := bytes.Buffer{} + + escapeChar := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteByte(c) + } + + escapeHex := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteString(enchex.EncodeToString([]byte{c})) + } + + for i := 0; i < len(value); i++ { + char := value[i] + if i == 0 && char == ' ' || char == '#' { + // Special case leading space or number sign. + escapeChar(char) + continue + } + if i == len(value)-1 && char == ' ' { + // Special case trailing space. + escapeChar(char) + continue + } + + switch char { + case '"', '+', ',', ';', '<', '>', '\\': + // Each of these special characters must be escaped. + escapeChar(char) + continue + } + + if char < ' ' || char > '~' { + // All special character escapes are handled first + // above. All bytes less than ASCII SPACE and all bytes + // greater than ASCII TILDE must be hex-escaped. + escapeHex(char) + continue + } + + // Any other character does not require escaping. + encodedBuf.WriteByte(char) + } + + return encodedBuf.String() +} + // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 type RelativeDN struct { Attributes []*AttributeTypeAndValue } +// String returns a normalized string representation of this relative DN which +// is the a join of all attributes (sorted in increasing order) with a "+". +func (r *RelativeDN) String() string { + attrs := make([]string, len(r.Attributes)) + for i := range r.Attributes { + attrs[i] = r.Attributes[i].String() + } + sort.Strings(attrs) + return strings.Join(attrs, "+") +} + // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 type DN struct { RDNs []*RelativeDN } +// String returns a normalized string representation of this DN which is the +// join of all relative DNs with a ",". +func (d *DN) String() string { + rdns := make([]string, len(d.RDNs)) + for i := range d.RDNs { + rdns[i] = d.RDNs[i].String() + } + return strings.Join(rdns, ",") +} + // ParseDN returns a distinguishedName or an error. // The function respects https://tools.ietf.org/html/rfc4514 func ParseDN(str string) (*DN, error) { diff --git a/dn_test.go b/dn_test.go index 6a82c72d..14f6f380 100644 --- a/dn_test.go +++ b/dn_test.go @@ -148,8 +148,8 @@ func TestDNEqual(t *testing.T) { }, // Difference in leading/trailing chars is ignored { - "cn=John Doe, ou=People, dc=sun.com", - "cn=John Doe,ou=People,dc=sun.com", + "cn=\\ John\\20Doe, ou=People, dc=sun.com", + "cn= \\ John Doe,ou=People,dc=sun.com", true, }, // Difference in values is significant @@ -174,11 +174,16 @@ func TestDNEqual(t *testing.T) { continue } if expected, actual := tc.Equal, a.Equal(b); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) continue } if expected, actual := tc.Equal, b.Equal(a); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) + continue + } + + if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual { + t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual) continue } } diff --git a/v3/dn.go b/v3/dn.go index d802580e..adbdb786 100644 --- a/v3/dn.go +++ b/v3/dn.go @@ -5,6 +5,7 @@ import ( enchex "encoding/hex" "errors" "fmt" + "sort" "strings" ber "github.com/go-asn1-ber/asn1-ber" @@ -18,16 +19,95 @@ type AttributeTypeAndValue struct { Value string } +// String returns a normalized string representation of this attribute type and +// value pair which is the a lowercased join of the Type and Value with a "=". +func (a *AttributeTypeAndValue) String() string { + return strings.ToLower(a.Type) + "=" + a.encodeValue() +} + +func (a *AttributeTypeAndValue) encodeValue() string { + // Normalize the value first. + // value := strings.ToLower(a.Value) + value := a.Value + + encodedBuf := bytes.Buffer{} + + escapeChar := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteByte(c) + } + + escapeHex := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteString(enchex.EncodeToString([]byte{c})) + } + + for i := 0; i < len(value); i++ { + char := value[i] + if i == 0 && char == ' ' || char == '#' { + // Special case leading space or number sign. + escapeChar(char) + continue + } + if i == len(value)-1 && char == ' ' { + // Special case trailing space. + escapeChar(char) + continue + } + + switch char { + case '"', '+', ',', ';', '<', '>', '\\': + // Each of these special characters must be escaped. + escapeChar(char) + continue + } + + if char < ' ' || char > '~' { + // All special character escapes are handled first + // above. All bytes less than ASCII SPACE and all bytes + // greater than ASCII TILDE must be hex-escaped. + escapeHex(char) + continue + } + + // Any other character does not require escaping. + encodedBuf.WriteByte(char) + } + + return encodedBuf.String() +} + // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 type RelativeDN struct { Attributes []*AttributeTypeAndValue } +// String returns a normalized string representation of this relative DN which +// is the a join of all attributes (sorted in increasing order) with a "+". +func (r *RelativeDN) String() string { + attrs := make([]string, len(r.Attributes)) + for i := range r.Attributes { + attrs[i] = r.Attributes[i].String() + } + sort.Strings(attrs) + return strings.Join(attrs, "+") +} + // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 type DN struct { RDNs []*RelativeDN } +// String returns a normalized string representation of this DN which is the +// join of all relative DNs with a ",". +func (d *DN) String() string { + rdns := make([]string, len(d.RDNs)) + for i := range d.RDNs { + rdns[i] = d.RDNs[i].String() + } + return strings.Join(rdns, ",") +} + // ParseDN returns a distinguishedName or an error. // The function respects https://tools.ietf.org/html/rfc4514 func ParseDN(str string) (*DN, error) { @@ -84,7 +164,7 @@ func ParseDN(str string) (*DN, error) { if len(str) > i+1 && str[i+1] == '#' { i += 2 index := strings.IndexAny(str[i:], ",+") - data := str + var data string if index > 0 { data = str[i : i+index] } else { diff --git a/v3/dn_test.go b/v3/dn_test.go index 6a82c72d..14f6f380 100644 --- a/v3/dn_test.go +++ b/v3/dn_test.go @@ -148,8 +148,8 @@ func TestDNEqual(t *testing.T) { }, // Difference in leading/trailing chars is ignored { - "cn=John Doe, ou=People, dc=sun.com", - "cn=John Doe,ou=People,dc=sun.com", + "cn=\\ John\\20Doe, ou=People, dc=sun.com", + "cn= \\ John Doe,ou=People,dc=sun.com", true, }, // Difference in values is significant @@ -174,11 +174,16 @@ func TestDNEqual(t *testing.T) { continue } if expected, actual := tc.Equal, a.Equal(b); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) continue } if expected, actual := tc.Equal, b.Equal(a); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) + continue + } + + if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual { + t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual) continue } }