mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Add DistinguishedName proto conversions (#65900)
* Add DistinguishedName proto conversions * Mention asn1.parseBase128Int validation of OIDs
This commit is contained in:
+102
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user