22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System . Collections . Generic ;
5+ using System . Diagnostics ;
56using System . Formats . Asn1 ;
67using System . IO ;
78using System . Linq ;
@@ -1043,6 +1044,322 @@ public static void PolicyConstraints_Mapped()
10431044 }
10441045 }
10451046
1047+ [ Theory ]
1048+ [ MemberData ( nameof ( ChainPolicyMemberData ) ) ]
1049+ [ ActiveIssue ( "https://github.com/dotnet/runtime/issues/128890" , TestPlatforms . Android ) ]
1050+ public static void CertificatePolicyTest (
1051+ ChainPolicyTestCase testCase )
1052+ {
1053+ using ( testCase )
1054+ {
1055+ TestChain4 (
1056+ testCase . Root ,
1057+ testCase . HighIntermediate ,
1058+ testCase . LowIntermediate ,
1059+ testCase . EndEntity ,
1060+ PlatformPolicyConstraints ( testCase . ExpectedFlags ) ,
1061+ testCase . ConfigureCallback ) ;
1062+ }
1063+ }
1064+
1065+ public static IEnumerable < object [ ] > ChainPolicyMemberData ( )
1066+ {
1067+ foreach ( ChainPolicyTestCase testCase in TestCases ( ) )
1068+ {
1069+ yield return new object [ ] { testCase } ;
1070+ }
1071+
1072+ static IEnumerable < ChainPolicyTestCase > TestCases ( )
1073+ {
1074+ // Use the same keys for all chains, just to keep the total time low.
1075+ // There are enough cases here that keygen shows up in clock time.
1076+ RSA [ ] keys = [ RSA . Create ( 2048 ) , RSA . Create ( 2048 ) , RSA . Create ( 2048 ) , RSA . Create ( 2048 ) ] ;
1077+
1078+ // These test cases only show in results by their test case number,
1079+ // because describing them in words would be very verbose.
1080+ //
1081+ // Skipped (unyielded) cases need to reserve their numbers so they're
1082+ // the same on all platforms.
1083+ //
1084+ // Ideally, new cases are added with higher values, so that test history
1085+ // isn't comparing apples and oranges.
1086+
1087+ int caseId = 0 ;
1088+
1089+ // No chain.Policy.CertificatePolicy checks, EE uses the mapped policy identifier,
1090+ // intermediate requires that the EE certs have a policy extension.
1091+ //
1092+ // Despite the intermediate specifying a mapping when it's forbidden (inhibit<2),
1093+ // Everything is reported valid, because the EE cert policy C doesn't require the mapping.
1094+ for ( int rootInhibitMapping = 0 ; rootInhibitMapping <= 2 ; rootInhibitMapping ++ )
1095+ {
1096+ yield return ChainPolicyTestCase . Build (
1097+ caseId ++ ,
1098+ keys ,
1099+ rootInhibitMapping ,
1100+ 0 ,
1101+ [ ChainPolicyTestCase . PolicyB , ChainPolicyTestCase . PolicyC ] ,
1102+ [ ] ,
1103+ X509ChainStatusFlags . NoError ) ;
1104+ }
1105+
1106+ Debug . Assert ( caseId == 3 ) ;
1107+
1108+ // No chain.Policy.CertificatePolicy checks, EE uses the mapped policy identifier,
1109+ // intermediate requires that the EE certs have a policy extension.
1110+ //
1111+ // The EE policy is required, and the only policy in the EE cert is the mapped one,
1112+ // but that is forbidden by the intermediate's inhibit=<2, so it's an
1113+ // Issuance-Chain-Policy violation.
1114+ for ( int rootInhibitMapping = 0 ; rootInhibitMapping <= 2 ; rootInhibitMapping ++ )
1115+ {
1116+ // Windows seems to be the only OS capable of reporting this error.
1117+
1118+ yield return ChainPolicyTestCase . Build (
1119+ caseId ++ ,
1120+ keys ,
1121+ rootInhibitMapping ,
1122+ 0 ,
1123+ [ ChainPolicyTestCase . PolicyB ] ,
1124+ [ ] ,
1125+ rootInhibitMapping < 2 && OperatingSystem . IsWindows ( ) ?
1126+ X509ChainStatusFlags . NoIssuanceChainPolicy :
1127+ X509ChainStatusFlags . NoError ) ;
1128+ }
1129+
1130+ Debug . Assert ( caseId == 6 ) ;
1131+
1132+ // No chain.Policy.CertificatePolicy checks, EE uses the mapped policy identifier,
1133+ // intermediate does not require that the EE certs have a policy extension.
1134+ //
1135+ // Even though the intermediate has a disallowed mapping when inhibit=<2,
1136+ // since the EE isn't _required_ to have a policy, nothing was required to
1137+ // traverse the map.
1138+ for ( int rootInhibitMapping = 0 ; rootInhibitMapping <= 2 ; rootInhibitMapping ++ )
1139+ {
1140+ yield return ChainPolicyTestCase . Build (
1141+ caseId ++ ,
1142+ keys ,
1143+ rootInhibitMapping ,
1144+ - 1 ,
1145+ [ ChainPolicyTestCase . PolicyB ] ,
1146+ [ ] ,
1147+ X509ChainStatusFlags . NoError ) ;
1148+ }
1149+
1150+ Debug . Assert ( caseId == 9 ) ;
1151+
1152+ // EE uses the mapped policy identifier,
1153+ // intermediate requires that the EE certs have a policy extension.
1154+ // Require that the EE cert is valid for only policy C (no mapping required).
1155+ //
1156+ // Despite the intermediate specifying a mapping when it's forbidden (inhibit=<2),
1157+ // Everything is reported valid, because the EE cert policy C doesn't require the mapping.
1158+ for ( int rootInhibitMapping = 0 ; rootInhibitMapping <= 2 ; rootInhibitMapping ++ )
1159+ {
1160+ yield return ChainPolicyTestCase . Build (
1161+ caseId ++ ,
1162+ keys ,
1163+ rootInhibitMapping ,
1164+ 0 ,
1165+ [ ChainPolicyTestCase . PolicyB , ChainPolicyTestCase . PolicyC ] ,
1166+ [ ChainPolicyTestCase . PolicyC ] ,
1167+ X509ChainStatusFlags . NoError ) ;
1168+ }
1169+
1170+ Debug . Assert ( caseId == 12 ) ;
1171+
1172+ // EE uses the mapped policy identifier,
1173+ // intermediate does not require that the EE certs have a policy extension.
1174+ // Require that the EE cert is valid for only policy C (no mapping required).
1175+ //
1176+ // Despite the intermediate specifying a mapping when it's forbidden (inhibit=<2),
1177+ // Everything is reported valid, because the EE cert policy C doesn't require the mapping.
1178+ for ( int rootInhibitMapping = 0 ; rootInhibitMapping <= 2 ; rootInhibitMapping ++ )
1179+ {
1180+ yield return ChainPolicyTestCase . Build (
1181+ caseId ++ ,
1182+ keys ,
1183+ rootInhibitMapping ,
1184+ - 1 ,
1185+ [ ChainPolicyTestCase . PolicyB , ChainPolicyTestCase . PolicyC ] ,
1186+ [ ChainPolicyTestCase . PolicyC ] ,
1187+ X509ChainStatusFlags . NoError ) ;
1188+ }
1189+
1190+ Debug . Assert ( caseId == 15 ) ;
1191+
1192+ // EE uses the mapped policy identifier,
1193+ // intermediate requires that the EE certs have a policy extension.
1194+ // Require that the EE cert is valid for only policy A (which it calls C).
1195+ //
1196+ // Since this requires traversing the mapping, it's NotValidForUsage whenever
1197+ // the mapping was disallowed (inhibit<2).
1198+ for ( int rootInhibitMapping = 0 ; rootInhibitMapping <= 2 ; rootInhibitMapping ++ )
1199+ {
1200+ yield return ChainPolicyTestCase . Build (
1201+ caseId ++ ,
1202+ keys ,
1203+ rootInhibitMapping ,
1204+ 0 ,
1205+ [ ChainPolicyTestCase . PolicyB , ChainPolicyTestCase . PolicyC ] ,
1206+ [ ChainPolicyTestCase . PolicyA ] ,
1207+ rootInhibitMapping < 2 ?
1208+ X509ChainStatusFlags . NotValidForUsage :
1209+ X509ChainStatusFlags . NoError ) ;
1210+ }
1211+
1212+ Debug . Assert ( caseId == 18 ) ;
1213+
1214+ foreach ( RSA key in keys )
1215+ {
1216+ key . Dispose ( ) ;
1217+ }
1218+ }
1219+ }
1220+
1221+ public sealed class ChainPolicyTestCase : IDisposable
1222+ {
1223+ internal const string PolicyA = "0.1.2.3" ;
1224+ internal const string PolicyB = "1.2.3.4" ;
1225+ internal const string PolicyC = "2.3.4.5" ;
1226+
1227+ private int _number ;
1228+ private string [ ] _eePoliciesToCheck ;
1229+
1230+ internal X509ChainStatusFlags ExpectedFlags { get ; private set ; }
1231+ internal X509Certificate2 Root { get ; private set ; }
1232+ internal X509Certificate2 HighIntermediate { get ; private set ; }
1233+ internal X509Certificate2 LowIntermediate { get ; private set ; }
1234+ internal X509Certificate2 EndEntity { get ; private set ; }
1235+
1236+ private ChainPolicyTestCase ( )
1237+ {
1238+ }
1239+
1240+ public Action < X509ChainPolicy > ConfigureCallback
1241+ {
1242+ get
1243+ {
1244+ if ( _eePoliciesToCheck . Length == 0 )
1245+ {
1246+ return null ;
1247+ }
1248+
1249+ return policy =>
1250+ {
1251+ foreach ( string policyOid in _eePoliciesToCheck )
1252+ {
1253+ policy . CertificatePolicy . Add ( new Oid ( policyOid , null ) ) ;
1254+ }
1255+ } ;
1256+ }
1257+ }
1258+
1259+ public void Dispose ( )
1260+ {
1261+ Root ? . Dispose ( ) ;
1262+ HighIntermediate ? . Dispose ( ) ;
1263+ LowIntermediate ? . Dispose ( ) ;
1264+ EndEntity ? . Dispose ( ) ;
1265+ }
1266+
1267+ internal static ChainPolicyTestCase Build (
1268+ int number ,
1269+ RSA [ ] keys ,
1270+ int rootInhibitMapping ,
1271+ int intermediateRequireExplicit ,
1272+ string [ ] eePolicies ,
1273+ string [ ] eePoliciesToCheck ,
1274+ X509ChainStatusFlags expectedFlags )
1275+ {
1276+ X509Extension [ ] rootExtensions = new [ ]
1277+ {
1278+ BasicConstraintsCA ,
1279+ MaybePolicyConstraints ( inhibitPolicyMappingSkipCerts : rootInhibitMapping ) ,
1280+ } ;
1281+
1282+ X509Extension [ ] highImedExtensions = new [ ]
1283+ {
1284+ BasicConstraintsCA ,
1285+ // The "any" policy.
1286+ BuildPolicyByIdentifiers ( "2.5.29.32.0" ) ,
1287+ } ;
1288+
1289+ X509Extension [ ] lowImedExtensions = new [ ]
1290+ {
1291+ BasicConstraintsCA ,
1292+ MaybePolicyConstraints ( requireExplicitPolicySkipCerts : intermediateRequireExplicit ) ,
1293+ BuildPolicyByIdentifiers ( PolicyA , PolicyC ) ,
1294+ BuildPolicyMappings ( ( PolicyA , PolicyB ) ) ,
1295+ } ;
1296+
1297+ X509Extension [ ] endEntityExtensions = new [ ]
1298+ {
1299+ BasicConstraintsEndEntity ,
1300+ MaybePolicies ( eePolicies ) ,
1301+ } ;
1302+
1303+ X509Certificate2 [ ] certs = new X509Certificate2 [ 4 ] ;
1304+
1305+ TestDataGenerator . MakeTestChain (
1306+ keys ,
1307+ certs ,
1308+ endEntityExtensions ,
1309+ [ lowImedExtensions , highImedExtensions ] ,
1310+ rootExtensions ,
1311+ $ "{ nameof ( ChainPolicyTestCase ) } -{ number } ") ;
1312+
1313+ return new ChainPolicyTestCase
1314+ {
1315+ _number = number ,
1316+ EndEntity = certs [ 0 ] ,
1317+ LowIntermediate = certs [ 1 ] ,
1318+ HighIntermediate = certs [ 2 ] ,
1319+ Root = certs [ 3 ] ,
1320+ _eePoliciesToCheck = eePoliciesToCheck ,
1321+ ExpectedFlags = expectedFlags ,
1322+ } ;
1323+
1324+ static X509Extension MaybePolicies ( string [ ] policyOids )
1325+ {
1326+ if ( policyOids . Length == 0 )
1327+ {
1328+ return null ;
1329+ }
1330+
1331+ return BuildPolicyByIdentifiers ( policyOids ) ;
1332+ }
1333+
1334+ static X509Extension MaybePolicyConstraints (
1335+ int requireExplicitPolicySkipCerts = - 1 ,
1336+ int inhibitPolicyMappingSkipCerts = - 1 )
1337+ {
1338+ if ( inhibitPolicyMappingSkipCerts >= 0 && requireExplicitPolicySkipCerts >= 0 )
1339+ {
1340+ return BuildPolicyConstraints ( inhibitPolicyMappingSkipCerts , requireExplicitPolicySkipCerts ) ;
1341+ }
1342+
1343+ if ( inhibitPolicyMappingSkipCerts >= 0 )
1344+ {
1345+ return BuildPolicyConstraints ( inhibitPolicyMappingSkipCerts : inhibitPolicyMappingSkipCerts ) ;
1346+ }
1347+
1348+ if ( requireExplicitPolicySkipCerts >= 0 )
1349+ {
1350+ return BuildPolicyConstraints ( requireExplicitPolicySkipCerts : requireExplicitPolicySkipCerts ) ;
1351+ }
1352+
1353+ return null ;
1354+ }
1355+ }
1356+
1357+ public override string ToString ( )
1358+ {
1359+ return $ "CaseID { _number } ";
1360+ }
1361+ }
1362+
10461363 public enum BuildChainWithNotSignatureValidTest : int
10471364 {
10481365 TrustedRoot ,
@@ -1349,11 +1666,43 @@ private static X509Extension BuildPolicyMappings(
13491666 return new X509Extension ( "2.5.29.33" , writer . Encode ( ) , critical : true ) ;
13501667 }
13511668
1669+ private static void TestChain4 (
1670+ X509Certificate2 rootCertificate ,
1671+ X509Certificate2 highIntermediateCertificate ,
1672+ X509Certificate2 lowIntermediateCertificate ,
1673+ X509Certificate2 endEntityCertificate ,
1674+ X509ChainStatusFlags expectedFlags = X509ChainStatusFlags . NoError ,
1675+ Action < X509ChainPolicy > configurePolicy = null )
1676+ {
1677+ using ( ChainHolder chainHolder = new ChainHolder ( ) )
1678+ {
1679+ X509Chain chain = chainHolder . Chain ;
1680+ chain . ChainPolicy . RevocationMode = X509RevocationMode . NoCheck ;
1681+ chain . ChainPolicy . VerificationTime = endEntityCertificate . NotBefore . AddSeconds ( 1 ) ;
1682+ chain . ChainPolicy . TrustMode = X509ChainTrustMode . CustomRootTrust ;
1683+ chain . ChainPolicy . CustomTrustStore . Add ( rootCertificate ) ;
1684+ chain . ChainPolicy . ExtraStore . Add ( highIntermediateCertificate ) ;
1685+ chain . ChainPolicy . ExtraStore . Add ( lowIntermediateCertificate ) ;
1686+ configurePolicy ? . Invoke ( chain . ChainPolicy ) ;
1687+
1688+ bool result = chain . Build ( endEntityCertificate ) ;
1689+ bool expected = expectedFlags == X509ChainStatusFlags . NoError ;
1690+ X509ChainStatusFlags actualFlags = chain . AllStatusFlags ( ) ;
1691+ int depth = chain . ChainElements ? . Count ?? 0 ;
1692+ Assert . True ( result == expected , $ "chain.Build returned { result } with flags ({ actualFlags } ) and depth { depth } when ({ expectedFlags } ) with depth 4 was expected") ;
1693+
1694+ Assert . True (
1695+ actualFlags . HasFlag ( expectedFlags ) ,
1696+ $ "Expected Flags: \" { expectedFlags } \" ; Actual Flags: \" { actualFlags } \" ") ;
1697+ }
1698+ }
1699+
13521700 private static void TestChain3 (
13531701 X509Certificate2 rootCertificate ,
13541702 X509Certificate2 intermediateCertificate ,
13551703 X509Certificate2 endEntityCertificate ,
1356- X509ChainStatusFlags expectedFlags = X509ChainStatusFlags . NoError )
1704+ X509ChainStatusFlags expectedFlags = X509ChainStatusFlags . NoError ,
1705+ Action < X509ChainPolicy > configurePolicy = null )
13571706 {
13581707 using ( ChainHolder chainHolder = new ChainHolder ( ) )
13591708 {
@@ -1363,10 +1712,13 @@ private static void TestChain3(
13631712 chain . ChainPolicy . TrustMode = X509ChainTrustMode . CustomRootTrust ;
13641713 chain . ChainPolicy . CustomTrustStore . Add ( rootCertificate ) ;
13651714 chain . ChainPolicy . ExtraStore . Add ( intermediateCertificate ) ;
1715+ configurePolicy ? . Invoke ( chain . ChainPolicy ) ;
13661716
13671717 bool result = chain . Build ( endEntityCertificate ) ;
1718+ bool expected = expectedFlags == X509ChainStatusFlags . NoError ;
13681719 X509ChainStatusFlags actualFlags = chain . AllStatusFlags ( ) ;
1369- Assert . True ( result == ( expectedFlags == X509ChainStatusFlags . NoError ) , $ "chain.Build ({ actualFlags } )") ;
1720+ int depth = chain . ChainElements ? . Count ?? 0 ;
1721+ Assert . True ( result == expected , $ "chain.Build returned { result } with flags ({ actualFlags } ) and depth { depth } when ({ expectedFlags } ) with depth 3 was expected") ;
13701722
13711723 Assert . True (
13721724 actualFlags . HasFlag ( expectedFlags ) ,
0 commit comments