diff --git a/pkg/policy/policySpecification_test.go b/pkg/policy/policySpecification_test.go index c4f2ca53..e095b18c 100644 --- a/pkg/policy/policySpecification_test.go +++ b/pkg/policy/policySpecification_test.go @@ -259,6 +259,55 @@ func TestBuildPolicySpecificationForTPPLocked(t *testing.T) { } } +// TestBuildPolicySpecificationForTPPPkixParameterSet covers TPP 25.1+ policy folders, which lock +// allowed key algorithms via KeyPair.PkixParameterSet.Values instead of the deprecated +// KeyAlgorithm/KeySize/EllipticCurve Locked flags. +func TestBuildPolicySpecificationForTPPPkixParameterSet(t *testing.T) { + tppPolicy := getPolicyResponse(true) + tppPolicy.KeyPairResponse.PkixParameterSet = LockedArrayAttribute{ + Locked: true, + Value: []string{ + "1.3.6.1.4.1.28783.10.1.1.4096", + "1.3.6.1.4.1.28783.10.1.1.8192", + }, + } + + ps, err := BuildPolicySpecificationForTPP(CheckPolicyResponse{Policy: &tppPolicy}) + if err != nil { + t.Fatalf("Error building policy specification \nError: %s", err) + } + + if ps.Policy == nil || ps.Policy.KeyPair == nil { + t.Fatal("expected a key pair policy to be set") + } + keyPair := ps.Policy.KeyPair + if len(keyPair.KeyTypes) != 1 || keyPair.KeyTypes[0] != "RSA" { + t.Fatalf("expected KeyTypes [RSA], got %v", keyPair.KeyTypes) + } + if len(keyPair.RsaKeySizes) != 2 || keyPair.RsaKeySizes[0] != 4096 || keyPair.RsaKeySizes[1] != 8192 { + t.Fatalf("expected RsaKeySizes [4096 8192], got %v", keyPair.RsaKeySizes) + } + if len(keyPair.EllipticCurves) != 0 { + t.Fatalf("expected no EllipticCurves, got %v", keyPair.EllipticCurves) + } +} + +func TestBuildPolicySpecificationForTPPPkixParameterSetUnrecognisedOID(t *testing.T) { + tppPolicy := getPolicyResponse(true) + tppPolicy.KeyPairResponse.PkixParameterSet = LockedArrayAttribute{ + Locked: true, + Value: []string{"1.2.3.4.5"}, + } + + _, err := BuildPolicySpecificationForTPP(CheckPolicyResponse{Policy: &tppPolicy}) + if err == nil { + t.Fatal("expected an error for an unrecognised PKIX parameter set OID") + } + if !strings.Contains(err.Error(), "unrecognised PKIX parameter set OID") { + t.Fatalf("unexpected error message: %v", err) + } +} + func TestGetZoneInfo(t *testing.T) { originalAPP := "DevOps" originalCit := "Open Source" diff --git a/pkg/policy/policyStructures.go b/pkg/policy/policyStructures.go index 002aa74a..63cbff17 100644 --- a/pkg/policy/policyStructures.go +++ b/pkg/policy/policyStructures.go @@ -244,6 +244,11 @@ type KeyPairResponse struct { KeyAlgorithm LockedAttribute `json:"KeyAlgorithm"` KeySize LockedIntAttribute `json:"KeySize"` EllipticCurve LockedAttribute `json:"EllipticCurve"` + // PkixParameterSet lists the PKIX OIDs of the key algorithms allowed by policy. Available + // from TPP 25.1 onwards, it supersedes KeyAlgorithm/KeySize/EllipticCurve above, which TPP + // no longer locks once a policy folder's allowed algorithms are configured via the newer + // AlgorithmSelector API. + PkixParameterSet LockedArrayAttribute `json:"PkixParameterSet"` } type SubjectResponse struct { diff --git a/pkg/policy/policyUtils.go b/pkg/policy/policyUtils.go index 76b66068..e5f1f44a 100644 --- a/pkg/policy/policyUtils.go +++ b/pkg/policy/policyUtils.go @@ -31,6 +31,7 @@ var KeyAlgorithmsToPKIX = map[string]map[string]string{ "2048": "1.3.6.1.4.1.28783.10.1.1.2048", "3072": "1.3.6.1.4.1.28783.10.1.1.3072", "4096": "1.3.6.1.4.1.28783.10.1.1.4096", + "8192": "1.3.6.1.4.1.28783.10.1.1.8192", }, "ECC": { "P256": "1.3.6.1.4.1.28783.10.2.1.256", @@ -39,6 +40,27 @@ var KeyAlgorithmsToPKIX = map[string]map[string]string{ }, } +// PkixOidKeyAlgorithm describes the key type/size/curve represented by a PKIX parameter set OID. +type PkixOidKeyAlgorithm struct { + KeyType string // "RSA" or "ECDSA" + KeySize int // populated for RSA + Curve string // populated for ECDSA, e.g. "P256" +} + +// PkixToKeyAlgorithms is the reverse of KeyAlgorithmsToPKIX: it maps a PKIX parameter set OID, as +// returned by TPP 25.1+'s AlgorithmSelector API (and surfaced via Certificates/CheckPolicy's +// KeyPair.PkixParameterSet.Values), back to the key type/size/curve it represents. +var PkixToKeyAlgorithms = map[string]PkixOidKeyAlgorithm{ + "1.3.6.1.4.1.28783.10.1.1.1024": {KeyType: "RSA", KeySize: 1024}, + "1.3.6.1.4.1.28783.10.1.1.2048": {KeyType: "RSA", KeySize: 2048}, + "1.3.6.1.4.1.28783.10.1.1.3072": {KeyType: "RSA", KeySize: 3072}, + "1.3.6.1.4.1.28783.10.1.1.4096": {KeyType: "RSA", KeySize: 4096}, + "1.3.6.1.4.1.28783.10.1.1.8192": {KeyType: "RSA", KeySize: 8192}, + "1.3.6.1.4.1.28783.10.2.1.256": {KeyType: "ECDSA", Curve: "P256"}, + "1.3.6.1.4.1.28783.10.2.1.384": {KeyType: "ECDSA", Curve: "P384"}, + "1.3.6.1.4.1.28783.10.2.1.521": {KeyType: "ECDSA", Curve: "P521"}, +} + func GetFileType(f string) string { extension := filepath.Ext(f) @@ -596,34 +618,60 @@ func BuildPolicySpecificationForTPP(checkPolicyResp CheckPolicyResponse) (*Polic //resolve key pair's attributes - //resolve keyTypes - if policy.KeyPairResponse.KeyAlgorithm.Value != "" { - if policy.KeyPairResponse.KeyAlgorithm.Locked { - keyPair.KeyTypes = []string{policy.KeyPairResponse.KeyAlgorithm.Value} - } else { - shouldCreateDefKeyPair = true - defaultKeyPair.KeyType = &policy.KeyPairResponse.KeyAlgorithm.Value - } - } - - if strings.ToUpper(policy.KeyPairResponse.KeyAlgorithm.Value) == "RSA" { - //resolve rsaKeySizes - if policy.KeyPairResponse.KeySize.Value > 0 { - if policy.KeyPairResponse.KeySize.Locked { - keyPair.RsaKeySizes = []int{policy.KeyPairResponse.KeySize.Value} - } else { - shouldCreateDefKeyPair = true - defaultKeyPair.RsaKeySize = &policy.KeyPairResponse.KeySize.Value + if policy.KeyPairResponse.PkixParameterSet.Locked && len(policy.KeyPairResponse.PkixParameterSet.Value) > 0 { + //TPP 25.1+: allowed key algorithms are locked via the PKIX parameter set OID list rather + //than the deprecated KeyAlgorithm/KeySize/EllipticCurve fields below. + var keyTypes []string + var rsaKeySizes []int + var curves []string + for _, oid := range policy.KeyPairResponse.PkixParameterSet.Value { + alg, ok := PkixToKeyAlgorithms[oid] + if !ok { + return nil, fmt.Errorf("policy allows unrecognised PKIX parameter set OID %q; vcert's OID table may need updating", oid) + } + if !existValueInArray(keyTypes, alg.KeyType) { + keyTypes = append(keyTypes, alg.KeyType) + } + switch alg.KeyType { + case "RSA": + rsaKeySizes = append(rsaKeySizes, alg.KeySize) + case "ECDSA": + curves = append(curves, alg.Curve) } } + keyPair.KeyTypes = keyTypes + keyPair.RsaKeySizes = rsaKeySizes + keyPair.EllipticCurves = curves } else { - //resolve ellipticCurve - if policy.KeyPairResponse.EllipticCurve.Value != "" { - if policy.KeyPairResponse.EllipticCurve.Locked { - keyPair.EllipticCurves = []string{policy.KeyPairResponse.EllipticCurve.Value} + //resolve keyTypes + if policy.KeyPairResponse.KeyAlgorithm.Value != "" { + if policy.KeyPairResponse.KeyAlgorithm.Locked { + keyPair.KeyTypes = []string{policy.KeyPairResponse.KeyAlgorithm.Value} } else { shouldCreateDefKeyPair = true - defaultKeyPair.EllipticCurve = &policy.KeyPairResponse.EllipticCurve.Value + defaultKeyPair.KeyType = &policy.KeyPairResponse.KeyAlgorithm.Value + } + } + + if strings.ToUpper(policy.KeyPairResponse.KeyAlgorithm.Value) == "RSA" { + //resolve rsaKeySizes + if policy.KeyPairResponse.KeySize.Value > 0 { + if policy.KeyPairResponse.KeySize.Locked { + keyPair.RsaKeySizes = []int{policy.KeyPairResponse.KeySize.Value} + } else { + shouldCreateDefKeyPair = true + defaultKeyPair.RsaKeySize = &policy.KeyPairResponse.KeySize.Value + } + } + } else { + //resolve ellipticCurve + if policy.KeyPairResponse.EllipticCurve.Value != "" { + if policy.KeyPairResponse.EllipticCurve.Locked { + keyPair.EllipticCurves = []string{policy.KeyPairResponse.EllipticCurve.Value} + } else { + shouldCreateDefKeyPair = true + defaultKeyPair.EllipticCurve = &policy.KeyPairResponse.EllipticCurve.Value + } } } } diff --git a/pkg/venafi/tpp/tpp.go b/pkg/venafi/tpp/tpp.go index f47b9a3c..106c8fff 100644 --- a/pkg/venafi/tpp/tpp.go +++ b/pkg/venafi/tpp/tpp.go @@ -34,6 +34,7 @@ import ( "github.com/Venafi/vcert/v5/pkg/certificate" "github.com/Venafi/vcert/v5/pkg/endpoint" headers "github.com/Venafi/vcert/v5/pkg/httputils" + "github.com/Venafi/vcert/v5/pkg/policy" ) const defaultKeySize = 2048 @@ -828,6 +829,14 @@ type serverPolicy struct { Locked bool Value string } + // PkixParameterSet lists the PKIX OIDs of the key algorithms allowed by policy. Available + // from TPP 25.1 onwards, it supersedes KeyAlgorithm/KeySize/EllipticCurve above, which TPP + // no longer locks once a policy folder's allowed algorithms are configured via the newer + // AlgorithmSelector API. + PkixParameterSet struct { + Locked bool + Values []string + } } ManagementType _strValue @@ -853,6 +862,40 @@ type serverPolicy struct { WildcardsAllowed bool } +// allowedKeyConfigurationsFromPkixParameterSet decodes a list of PKIX parameter set OIDs, as returned by +// TPP 25.1+'s Certificates/CheckPolicy KeyPair.PkixParameterSet.Values, into AllowedKeyConfigurations. +// The OID table (policy.PkixToKeyAlgorithms) is shared with pkg/policy's getpolicy/setpolicy CLI path, +// which decodes the same OIDs from the same TPP API family. +func allowedKeyConfigurationsFromPkixParameterSet(oids []string) ([]endpoint.AllowedKeyConfiguration, error) { + var rsaSizes []int + var curves []certificate.EllipticCurve + for _, oid := range oids { + entry, ok := policy.PkixToKeyAlgorithms[oid] + if !ok { + return nil, fmt.Errorf("tpp: policy allows unrecognised PKIX parameter set OID %q; vcert's OID table may need updating", oid) + } + switch entry.KeyType { + case "RSA": + rsaSizes = append(rsaSizes, entry.KeySize) + case "ECDSA": + var curve certificate.EllipticCurve + if err := curve.Set(entry.Curve); err != nil { + return nil, fmt.Errorf("tpp: policy allows PKIX parameter set OID %q for an unsupported curve %q: %w", oid, entry.Curve, err) + } + curves = append(curves, curve) + } + } + + var configs []endpoint.AllowedKeyConfiguration + if len(rsaSizes) > 0 { + configs = append(configs, endpoint.AllowedKeyConfiguration{KeyType: certificate.KeyTypeRSA, KeySizes: rsaSizes}) + } + if len(curves) > 0 { + configs = append(configs, endpoint.AllowedKeyConfiguration{KeyType: certificate.KeyTypeECDSA, KeyCurves: curves}) + } + return configs, nil +} + func (sp serverPolicy) toZoneConfig(zc *endpoint.ZoneConfiguration) { zc.Country = sp.Subject.Country.Value zc.Organization = sp.Subject.Organization.Value @@ -981,7 +1024,13 @@ func (sp serverPolicy) toPolicy() (p endpoint.Policy) { } else { p.UpnSanRegExs = []string{} } - if sp.KeyPair.KeyAlgorithm.Locked { + if sp.KeyPair.PkixParameterSet.Locked && len(sp.KeyPair.PkixParameterSet.Values) > 0 { + configs, err := allowedKeyConfigurationsFromPkixParameterSet(sp.KeyPair.PkixParameterSet.Values) + if err != nil { + panic(err) + } + p.AllowedKeyConfigurations = append(p.AllowedKeyConfigurations, configs...) + } else if sp.KeyPair.KeyAlgorithm.Locked { var keyType certificate.KeyType if err := keyType.Set(sp.KeyPair.KeyAlgorithm.Value, sp.KeyPair.EllipticCurve.Value); err != nil { panic(err) diff --git a/pkg/venafi/tpp/tpp_test.go b/pkg/venafi/tpp/tpp_test.go index 60f6eedc..8977494d 100644 --- a/pkg/venafi/tpp/tpp_test.go +++ b/pkg/venafi/tpp/tpp_test.go @@ -18,6 +18,7 @@ package tpp import ( "crypto/x509" + "fmt" "net/http" "strings" "testing" @@ -282,6 +283,10 @@ func TestConvertServerPolicyToInternalPolicy(t *testing.T) { Locked bool Value string } + PkixParameterSet struct { + Locked bool + Values []string + } }{ KeyAlgorithm: _strValue{ Locked: true, @@ -326,6 +331,10 @@ func TestConvertServerPolicyToInternalPolicy(t *testing.T) { Locked bool Value string } + PkixParameterSet struct { + Locked bool + Values []string + } }{ KeyAlgorithm: _strValue{ Locked: true, @@ -370,6 +379,10 @@ func TestConvertServerPolicyToInternalPolicy(t *testing.T) { Locked bool Value string } + PkixParameterSet struct { + Locked bool + Values []string + } }{ KeyAlgorithm: _strValue{ Locked: false, @@ -431,3 +444,109 @@ func TestConvertServerPolicyToInternalPolicy(t *testing.T) { t.Fatalf("invalid SubjectCNRegexes[2], expected ^([\\p{L}\\p{N}-*]+\\.)+test3\\.com$, got %s", p.SubjectCNRegexes[2]) } } + +// TestConvertServerPolicyToInternalPolicy_PkixParameterSet covers TPP 25.1+ policy folders, which lock +// allowed key algorithms via KeyPair.PkixParameterSet.Values instead of the deprecated +// KeyAlgorithm/KeySize/EllipticCurve Locked flags. +func TestConvertServerPolicyToInternalPolicy_PkixParameterSet(t *testing.T) { + newServerPolicy := func(pkixLocked bool, pkixValues []string) serverPolicy { + var sp serverPolicy + sp.KeyPair.PkixParameterSet.Locked = pkixLocked + sp.KeyPair.PkixParameterSet.Values = pkixValues + return sp + } + + t.Run("single RSA size allowed", func(t *testing.T) { + sp := newServerPolicy(true, []string{"1.3.6.1.4.1.28783.10.1.1.4096"}) + p := sp.toPolicy() + if len(p.AllowedKeyConfigurations) != 1 { + t.Fatalf("expected 1 configuration, got %d", len(p.AllowedKeyConfigurations)) + } + k := p.AllowedKeyConfigurations[0] + if k.KeyType != certificate.KeyTypeRSA { + t.Fatalf("expected RSA, got %v", k.KeyType) + } + if len(k.KeySizes) != 1 || k.KeySizes[0] != 4096 { + t.Fatalf("expected [4096], got %v", k.KeySizes) + } + }) + + t.Run("multiple RSA sizes allowed", func(t *testing.T) { + sp := newServerPolicy(true, []string{ + "1.3.6.1.4.1.28783.10.1.1.4096", + "1.3.6.1.4.1.28783.10.1.1.8192", + }) + p := sp.toPolicy() + if len(p.AllowedKeyConfigurations) != 1 { + t.Fatalf("expected 1 configuration, got %d", len(p.AllowedKeyConfigurations)) + } + k := p.AllowedKeyConfigurations[0] + if k.KeyType != certificate.KeyTypeRSA { + t.Fatalf("expected RSA, got %v", k.KeyType) + } + if len(k.KeySizes) != 2 || k.KeySizes[0] != 4096 || k.KeySizes[1] != 8192 { + t.Fatalf("expected [4096 8192], got %v", k.KeySizes) + } + }) + + t.Run("single ECDSA curve allowed", func(t *testing.T) { + sp := newServerPolicy(true, []string{"1.3.6.1.4.1.28783.10.2.1.256"}) + p := sp.toPolicy() + if len(p.AllowedKeyConfigurations) != 1 { + t.Fatalf("expected 1 configuration, got %d", len(p.AllowedKeyConfigurations)) + } + k := p.AllowedKeyConfigurations[0] + if k.KeyType != certificate.KeyTypeECDSA { + t.Fatalf("expected ECDSA, got %v", k.KeyType) + } + if len(k.KeyCurves) != 1 || k.KeyCurves[0] != certificate.EllipticCurveP256 { + t.Fatalf("expected [P256], got %v", k.KeyCurves) + } + }) + + t.Run("mixed RSA and ECDSA allowed", func(t *testing.T) { + sp := newServerPolicy(true, []string{ + "1.3.6.1.4.1.28783.10.1.1.2048", + "1.3.6.1.4.1.28783.10.2.1.384", + }) + p := sp.toPolicy() + if len(p.AllowedKeyConfigurations) != 2 { + t.Fatalf("expected 2 configurations, got %d", len(p.AllowedKeyConfigurations)) + } + if p.AllowedKeyConfigurations[0].KeyType != certificate.KeyTypeRSA || len(p.AllowedKeyConfigurations[0].KeySizes) != 1 || p.AllowedKeyConfigurations[0].KeySizes[0] != 2048 { + t.Fatalf("unexpected RSA configuration: %+v", p.AllowedKeyConfigurations[0]) + } + if p.AllowedKeyConfigurations[1].KeyType != certificate.KeyTypeECDSA || len(p.AllowedKeyConfigurations[1].KeyCurves) != 1 || p.AllowedKeyConfigurations[1].KeyCurves[0] != certificate.EllipticCurveP384 { + t.Fatalf("unexpected ECDSA configuration: %+v", p.AllowedKeyConfigurations[1]) + } + }) + + t.Run("not locked falls back to legacy fields", func(t *testing.T) { + sp := newServerPolicy(false, []string{"1.3.6.1.4.1.28783.10.1.1.4096"}) + sp.KeyPair.KeyAlgorithm = _strValue{Locked: true, Value: "rsa"} + sp.KeyPair.KeySize.Locked = true + sp.KeyPair.KeySize.Value = 2048 + p := sp.toPolicy() + if len(p.AllowedKeyConfigurations) != 1 { + t.Fatalf("expected 1 configuration, got %d", len(p.AllowedKeyConfigurations)) + } + k := p.AllowedKeyConfigurations[0] + if len(k.KeySizes) != 4 || k.KeySizes[0] != 2048 { + t.Fatalf("expected fallback to legacy KeySize field, got %v", k.KeySizes) + } + }) + + t.Run("unrecognised OID panics with an actionable message", func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatal("expected a panic for an unrecognised OID") + } + if !strings.Contains(fmt.Sprint(r), "unrecognised PKIX parameter set OID") { + t.Fatalf("unexpected panic message: %v", r) + } + }() + sp := newServerPolicy(true, []string{"1.2.3.4.5"}) + sp.toPolicy() + }) +}