From c3b2f5114050c2250e927794e89c4ce1e0aae9c5 Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Wed, 7 Aug 2024 10:07:09 +0800 Subject: [PATCH] feat: support distinguished name state(S) (#432) Resolves #431 Signed-off-by: Junjie Gao --------- Signed-off-by: Junjie Gao --- internal/pkix/pkix.go | 11 +- internal/pkix/pkix_test.go | 143 +++++++++++++++++++++++ verifier/trustpolicy/trustpolicy_test.go | 4 +- 3 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 internal/pkix/pkix_test.go diff --git a/internal/pkix/pkix.go b/internal/pkix/pkix.go index c49cd790..964366c0 100644 --- a/internal/pkix/pkix.go +++ b/internal/pkix/pkix.go @@ -26,11 +26,10 @@ func ParseDistinguishedName(name string) (map[string]string, error) { return nil, fmt.Errorf("unsupported distinguished name (DN) %q: notation does not support x509.subject identities containing \"=#\"", name) } - mandatoryFields := []string{"C", "ST", "O"} attrKeyValue := make(map[string]string) dn, err := ldapv3.ParseDN(name) if err != nil { - return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err) + return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err) } for _, rdn := range dn.RDNs { @@ -39,6 +38,10 @@ func ParseDistinguishedName(name string) (map[string]string, error) { return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name) } for _, attribute := range rdn.Attributes { + // stateOrProvince name 'S' is an alias for 'ST' + if attribute.Type == "S" { + attribute.Type = "ST" + } if attrKeyValue[attribute.Type] == "" { attrKeyValue[attribute.Type] = attribute.Value } else { @@ -48,11 +51,13 @@ func ParseDistinguishedName(name string) (map[string]string, error) { } // Verify mandatory fields are present + mandatoryFields := []string{"C", "ST", "O"} for _, field := range mandatoryFields { if attrKeyValue[field] == "" { - return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field) + return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum", name, field) } } + // No errors return attrKeyValue, nil } diff --git a/internal/pkix/pkix_test.go b/internal/pkix/pkix_test.go new file mode 100644 index 00000000..3cedca27 --- /dev/null +++ b/internal/pkix/pkix_test.go @@ -0,0 +1,143 @@ +// Copyright The Notary Project 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 pkix + +import "testing" + +func TestParseDistinguishedName(t *testing.T) { + // Test cases + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "valid DN", + input: "C=US,ST=California,O=Notary Project", + wantErr: false, + }, + { + name: "valid DN with State alias", + input: "C=US,S=California,O=Notary Project", + wantErr: false, + }, + { + name: "invalid DN", + input: "C=US,ST=California", + wantErr: true, + }, + { + name: "invalid DN without State", + input: "C=US,O=Notary Project", + wantErr: true, + }, + { + name: "invalid DN without State", + input: "invalid", + wantErr: true, + }, + { + name: "duplicate RDN attribute", + input: "C=US,ST=California,O=Notary Project,S=California", + wantErr: true, + }, + { + name: "unsupported DN =#", + input: "C=US,ST=California,O=Notary Project=#", + wantErr: true, + }, + { + name: "multi-valued RDN attributes", + input: "OU=Sales+CN=J. Smith,DC=example,DC=net", + wantErr: true, + }, + } + + // Run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ParseDistinguishedName(tt.input) + if tt.wantErr != (err != nil) { + t.Errorf("ParseDistinguishedName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIsSubsetDN(t *testing.T) { + // Test cases + tests := []struct { + name string + dn1 map[string]string + dn2 map[string]string + want bool + }{ + { + name: "subset DN", + dn1: map[string]string{ + "C": "US", + "ST": "California", + "O": "Notary Project", + }, + dn2: map[string]string{ + "C": "US", + "ST": "California", + "O": "Notary Project", + "L": "Los Angeles", + }, + want: true, + }, + { + name: "not subset DN", + dn1: map[string]string{ + "C": "US", + "ST": "California", + "O": "Notary Project", + }, + dn2: map[string]string{ + "C": "US", + "ST": "California", + "O": "Notary Project 2", + "L": "Los Angeles", + "CN": "Notary", + }, + want: false, + }, + { + name: "not subset DN 2", + dn1: map[string]string{ + "C": "US", + "ST": "California", + "O": "Notary Project", + "CN": "Notary", + }, + dn2: map[string]string{ + "C": "US", + "ST": "California", + "O": "Notary Project", + "L": "Los Angeles", + }, + want: false, + }, + } + + // Run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsSubsetDN(tt.dn1, tt.dn2); got != tt.want { + t.Errorf("IsSubsetDN() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/verifier/trustpolicy/trustpolicy_test.go b/verifier/trustpolicy/trustpolicy_test.go index f58cadcc..381a9624 100644 --- a/verifier/trustpolicy/trustpolicy_test.go +++ b/verifier/trustpolicy/trustpolicy_test.go @@ -165,7 +165,7 @@ func TestValidateTrustedIdentities(t *testing.T) { // Validate x509.subject identities invalidDN := "x509.subject:,,," err = validateTrustedIdentities("test-statement-name", []string{invalidDN}) - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:,,,\" with invalid identity value: parsing distinguished name (DN) \",,,\" failed with err: incomplete type, value pair. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard" { + if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:,,,\" with invalid identity value: parsing distinguished name (DN) \",,,\" failed with err: incomplete type, value pair. A valid DN must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard" { t.Fatalf("invalid x509.subject identity should return error. Error : %q", err) } @@ -185,7 +185,7 @@ func TestValidateTrustedIdentities(t *testing.T) { // Validate mandatory RDNs invalidDN = "x509.subject:C=US,ST=WA" err = validateTrustedIdentities("test-statement-name", []string{invalidDN}) - if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,ST=WA\" with invalid identity value: distinguished name (DN) \"C=US,ST=WA\" has no mandatory RDN attribute for \"O\", it must contain 'C', 'ST', and 'O' RDN attributes at a minimum" { + if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,ST=WA\" with invalid identity value: distinguished name (DN) \"C=US,ST=WA\" has no mandatory RDN attribute for \"O\", it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum" { t.Fatalf("invalid x509.subject identity should return error. Error : %q", err) }