Add DistinguishedName proto conversions (#65900)

* Add DistinguishedName proto conversions

* Mention asn1.parseBase128Int validation of OIDs
This commit is contained in:
Alan Parra
2026-04-22 14:29:01 +00:00
committed by GitHub
parent 1429487b4b
commit fd2e58696d
2 changed files with 240 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package subca
import (
"crypto/x509/pkix"
"github.com/gravitational/trace"
subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1"
)
// RDNSequenceToDistinguishedNameProto converts a pkix.Name to a
// subcav1.DistinguishedName proto.
//
// All ATV values must be strings. Always returns non-nil on success.
func RDNSequenceToDistinguishedNameProto(rdns pkix.RDNSequence) (*subcav1.DistinguishedName, error) {
if len(rdns) == 0 {
return &subcav1.DistinguishedName{}, nil
}
// Usually it's one item per nameset. OK if not.
estimatedLen := len(rdns)
dn := &subcav1.DistinguishedName{
Names: make([]*subcav1.AttributeTypeAndValue, 0, estimatedLen),
}
for i, nameSet := range rdns {
for j, atv := range nameSet {
oid := make([]int32, len(atv.Type))
for i, x := range atv.Type {
// OID components are guaranteed to fit in a 32 bit integer as checked
// by encoding/asn1.parseBase128Int.
// https://cs.opensource.google/go/go/+/refs/tags/go1.26.2:src/encoding/asn1/asn1.go;l=323
oid[i] = int32(x)
}
val, ok := atv.Value.(string)
if !ok {
return nil, trace.BadParameter(
"rdns[%d][%d]: ATV value is not a string: %q=%v (%T)",
i, j,
atv.Type, atv.Value, atv.Value,
)
}
dn.Names = append(dn.Names, &subcav1.AttributeTypeAndValue{
Oid: oid,
Value: &val,
})
}
}
return dn, nil
}
// DistinguishedNameProtoToRDNSequence converts a subcav1.DistinguishedName
// proto to an RDNSequence.
//
// DistinguishedName protos are considered user-input, therefore this method
// applies more strict validation than its sibling
// [RDNSequenceToDistinguishedNameProto].
//
// A nil or empty DistinguishedName is considered invalid.
func DistinguishedNameProtoToRDNSequence(dn *subcav1.DistinguishedName) (pkix.RDNSequence, error) {
if dn == nil || len(dn.Names) == 0 {
return nil, trace.BadParameter("empty distinguished name")
}
rdns := make(pkix.RDNSequence, 0, len(dn.Names))
for i, atv := range dn.Names {
switch {
case len(atv.GetOid()) == 0:
return nil, trace.BadParameter("names[%d]: empty OID", i)
case atv.Value == nil:
return nil, trace.BadParameter("names[%d]: nil Value", i)
}
oid := make([]int, len(atv.Oid))
for j, x := range atv.Oid {
oid[j] = int(x)
}
rdns = append(rdns, pkix.RelativeDistinguishedNameSET{
pkix.AttributeTypeAndValue{
Type: oid,
Value: *atv.Value,
},
})
}
return rdns, nil
}
+138
View File
@@ -0,0 +1,138 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package subca_test
import (
"crypto/x509/pkix"
"encoding/asn1"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
subcav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/subca/v1"
"github.com/gravitational/teleport/lib/subca"
)
func TestDistinguishedName_roundtrip(t *testing.T) {
t.Parallel()
want := (pkix.Name{
Organization: []string{"Llama"},
CommonName: "Llamo",
ExtraNames: []pkix.AttributeTypeAndValue{
{Type: asn1.ObjectIdentifier{1, 2, 3, 4}, Value: "alpaca"},
},
}).ToRDNSequence()
dn, err := subca.RDNSequenceToDistinguishedNameProto(want)
require.NoError(t, err, "RDNSequenceToDistinguishedNameProto errored")
got, err := subca.DistinguishedNameProtoToRDNSequence(dn)
require.NoError(t, err, "DistinguishedNameProtoToRDNSequence errored")
assert.Equal(t, want, got, "DistinguishedName roundtrip mismatch")
}
func TestRDNSequenceToDistinguishedNameProto(t *testing.T) {
t.Parallel()
// Success tested by TestDistinguishedName_roundtrip.
t.Run("nil", func(t *testing.T) {
t.Parallel()
got, err := subca.RDNSequenceToDistinguishedNameProto(nil)
require.NoError(t, err)
want := &subcav1.DistinguishedName{}
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("DistinguishedName mismatch (-want +got)\n%s", diff)
}
})
t.Run("non-string value", func(t *testing.T) {
t.Parallel()
rdns := (pkix.Name{
ExtraNames: []pkix.AttributeTypeAndValue{
{Type: asn1.ObjectIdentifier{1, 2, 3, 4}, Value: "ok"}, // OK
{Type: asn1.ObjectIdentifier{1, 2, 3, 5}, Value: 42}, // NOK
},
}).ToRDNSequence()
_, err := subca.RDNSequenceToDistinguishedNameProto(rdns)
assert.ErrorContains(t, err, "not a string", "error mismatch")
})
}
func TestDistinguishedNameProtoToRDNSequence(t *testing.T) {
t.Parallel()
// Success tested by TestDistinguishedName_roundtrip.
exampleValue := "alpaca"
tests := []struct {
name string
dn *subcav1.DistinguishedName
wantErr string
}{
{
name: "nil",
wantErr: "empty distinguished name",
},
{
name: "empty",
dn: &subcav1.DistinguishedName{},
wantErr: "empty distinguished name",
},
{
name: "ATV nil",
dn: &subcav1.DistinguishedName{
Names: []*subcav1.AttributeTypeAndValue{
nil,
},
},
wantErr: "empty OID",
},
{
name: "ATV.OID empty",
dn: &subcav1.DistinguishedName{
Names: []*subcav1.AttributeTypeAndValue{
{Oid: []int32{}, Value: &exampleValue},
},
},
wantErr: "empty OID",
},
{
name: "ATV.Value nil",
dn: &subcav1.DistinguishedName{
Names: []*subcav1.AttributeTypeAndValue{
{Oid: []int32{1, 2, 3, 4}},
},
},
wantErr: "nil Value",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
_, err := subca.DistinguishedNameProtoToRDNSequence(test.dn)
assert.ErrorContains(t, err, test.wantErr, "error mismatch")
})
}
}