From 7814f9f345091d9d66ab547e75e38700e8d9dcf5 Mon Sep 17 00:00:00 2001 From: Uwe Gradenegger Date: Tue, 15 Feb 2022 20:50:07 +0100 Subject: [PATCH] Initial commit --- .gitignore | 344 +++++++++ LICENSE | 177 +++++ NOTICE | 13 + README.adoc | 289 ++++++++ TameMyCerts/AutoVersionIncrement.cs | 13 + TameMyCerts/AutoVersionIncrement.tt | 35 + TameMyCerts/CERTCLILib.dll | Bin 0 -> 11264 bytes TameMyCerts/CERTPOLICYLib.dll | Bin 0 -> 5120 bytes TameMyCerts/Headers.cs | 71 ++ TameMyCerts/IPAddressExtensions.cs | 91 +++ TameMyCerts/LocalizedStrings.Designer.cs | 347 +++++++++ TameMyCerts/LocalizedStrings.resx | 218 ++++++ TameMyCerts/Logger.cs | 165 +++++ TameMyCerts/Policy.cs | 472 ++++++++++++ TameMyCerts/PolicyManage.cs | 77 ++ TameMyCerts/Properties/AssemblyInfo.cs | 36 + TameMyCerts/RequestValidator.cs | 907 +++++++++++++++++++++++ TameMyCerts/SamplePolicy.xml | 153 ++++ TameMyCerts/TameMyCerts.csproj | 99 +++ TameMyCerts/TemplateInfo.cs | 88 +++ TameMyCerts/install.ps1 | 212 ++++++ TameMyCerts/make_debug.cmd | 15 + TameMyCerts/make_release.cmd | 24 + UnitTests/Properties/AssemblyInfo.cs | 20 + UnitTests/RequestValidatorTests.cs | 826 +++++++++++++++++++++ UnitTests/UnitTests.csproj | 74 ++ UnitTests/packages.config | 5 + 27 files changed, 4771 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.adoc create mode 100644 TameMyCerts/AutoVersionIncrement.cs create mode 100644 TameMyCerts/AutoVersionIncrement.tt create mode 100644 TameMyCerts/CERTCLILib.dll create mode 100644 TameMyCerts/CERTPOLICYLib.dll create mode 100644 TameMyCerts/Headers.cs create mode 100644 TameMyCerts/IPAddressExtensions.cs create mode 100644 TameMyCerts/LocalizedStrings.Designer.cs create mode 100644 TameMyCerts/LocalizedStrings.resx create mode 100644 TameMyCerts/Logger.cs create mode 100644 TameMyCerts/Policy.cs create mode 100644 TameMyCerts/PolicyManage.cs create mode 100644 TameMyCerts/Properties/AssemblyInfo.cs create mode 100644 TameMyCerts/RequestValidator.cs create mode 100644 TameMyCerts/SamplePolicy.xml create mode 100644 TameMyCerts/TameMyCerts.csproj create mode 100644 TameMyCerts/TemplateInfo.cs create mode 100644 TameMyCerts/install.ps1 create mode 100644 TameMyCerts/make_debug.cmd create mode 100644 TameMyCerts/make_release.cmd create mode 100644 UnitTests/Properties/AssemblyInfo.cs create mode 100644 UnitTests/RequestValidatorTests.cs create mode 100644 UnitTests/UnitTests.csproj create mode 100644 UnitTests/packages.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f94c6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,344 @@ +helpers/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +helpers/* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b6a5e27 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..a3f1332 --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Copyright 2021 Uwe Gradenegger + +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. \ No newline at end of file diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..8fb4f58 --- /dev/null +++ b/README.adoc @@ -0,0 +1,289 @@ += The "Tame My Certs" policy module for Active Directory Certificate Services + +TameMyCerts is a link:https://docs.microsoft.com/en-us/windows/win32/seccrypto/certificate-services-architecture[policy module^] for Microsoft (link:https://docs.microsoft.com/en-us/windows/win32/seccrypto/certificate-services[Active Directory Certificate Services, AD CS^]) enterprise certification authorities, written in C#. + +It shims the Windows Default policy module, that means it has the same functionality as the original one, but implements additional checks. This approach was adopted from an link:https://github.com/Sleepw4lker/capolmod[old C++ code sample that initially was published on Codeplex^]. + +The module supports checking certificate requests for certificate templates that allow the subject information to be specified by the enrolle against a defined policy. If any of the requested Subject string or Subject Alternative Name (SAN) violates the defined rules, the certificate request automatically gets denied by the certification authority. This concept is also known as applying "name constraints" to certificates. The module therefore helps you to tame your certs! + +== Value Proposition + +As a PKI operator, it is your responsibility to verify and confirm the enrollee's identity, and ensure he is permitted to request a certificate for the specified identity. As the certificate volume in a typical enterprise is quite high, it is common to automate the task of certificate issuance where possible. Active Directory Certificate Services offers the possibility to identify an enrollee by it's Active Directory identity (meaning the PKI delegates the identification job to AD) and build the certificate content based on this information. + +Sadly, there are many cases where this is not possible. In these cases, a certificate request is usually put into pending state so that a certificate manager can review and approve/deny the certificate request. However, this contradicts the goal of automatization. Also, putting such a certificate request into pending state is often not possible due to technical reasons. In these cases, the identification job is delegated entirely to the enrollee, which can lead to serious security issues: Any subject information (e.g. logon identities of administrative accounts in user certificates, or fraudulent web addresses in web server certificates) can be specified which opens a large security gap, waiting to be link:https://www.gradenegger.eu/?p=13269[abused by attackers^]. + +The TameMyCerts policy module addresses, amongst others, the following use cases: + +* Certificate issuance must be delegated to a 3rd party service, for example, Mobile Device Management (MDM) systems like AirWatch/Workspace One or MobileIron, link:https://social.technet.microsoft.com/wiki/contents/articles/9063.active-directory-certificate-services-ad-cs-network-device-enrollment-service-ndes.aspx[Network Device Enrollment Service (NDES)^] deployments or similar use cases that require the certificate template to be configured to have the enrollee supply the subject information with the certificate signing request in combination with direct certificate issuance. Without the module, there is absolutely no control over the issued certificate content. The module can also mitigate the problem that certificates may be inconsistent among platforms (e.g. having differing subject information on a mobile phone managed by MDM than on a PC that uses Autoenrollment because of inconsistent configuration settings on the MDM) by enforcing certificate content. +* Technical or legal requirements to allow any kind of subject RDN to be enabled for issuance on the certification authority (enabling link:https://www.gradenegger.eu/?p=952[CRLF_REBUILD_MODIFIED_SUBJECT_ONLY^] flag on the certification authority). Without the module, there is no control over which exact subject RDNs are allowed to be issued. +* Certificate templates configured to allow Elliptic Curve Cryptography (ECC) keys. Without the module, it would be possible that certificates get issued that use small RSA keys (e.g. 512 bit or even smaller) even though these would be not allowed in the certificate template configuration, as the Windows Default policy module link:https://www.gradenegger.eu/?p=14138[only validates the key length but not the key algorithm^]. + +=== Conclusion + +The TameMyCerts policy module allows fine-granular control about certificate content to greatly reduce attack surface, as well as ensuring that requested certificates conform to the organization's certificate policies. + +== Installing the module + +=== Supported Operating Systems + +The module was successfully tested with the following operating systems: + +* Windows Server 2022 +* Windows Server 2019 +* Windows Server 2016 +* Windows Server 2012 R2 (link:https://www.microsoft.com/en-us/download/details.aspx?id=48137[.NET Framework 4.6^] must be installed) + +Older operating systems are not supported. + +=== Installation + +To install the module, first create a directory on the certification authority where you intend to store the policy definition files. Then run *install.ps1* as Administrator. You must specify the *-PolicyDirectory* Parameter which specifies the local path for the XML config files you define for each certificate template. + +Example: + +.... +.\Install.ps1 -PolicyDirectory C:\PKIDATA\Policy +.... + +NOTE: The install script restarts the certification authority service during installation and uninstallation. + +NOTE: As the policy module daisy-chains the Windows default policy module, it also uses all of it's registry settings. Therefore, the install script copies this data from the Windows Default module's Registry key to a new one for the TameMyCerts policy module. Each change you perform with certutil commands that would configure the Windows Default policy module is now written to the TameMyCerts policy module's Registry key. If you should decide to uninstall it later on, the settings get copied back so that they stay consistent. + +NOTE: Do not install the module on a standalone certification authority. Only install it on certification authorities that are integrated into Active Directory (Enterprise CA). + +WARNING: Always create a backup before applying any change to the certification authority configuration. + +The script will register the module, create the required registry values and configure the policy module as the active one for the certification authority. + +=== Uninstalling + +To uninstall the module, run *install.ps1* as Administrator. You must specify the *-Uninstall* parameter. + +Example: + +.... +.\Install.ps1 -Uninstall +.... + +The script will unregister the module, copy the registry settings back and configure the Windows Default policy module as the active one. + +== Configuring the module + +Create a policy file in XML format for each certificate template you want to apply a policy for in the folder you specified during installation. Name the file exactly as the certificate template ("cn" LDAP attribute) that shall get examined. + +See the supplied link:TameMyCerts/SamplePolicy.xml[SamplePolicy.xml] for an example. + +NOTE: The policy module evaulates only certificate templates that are configured to have the enrollee specify subject information. Therefore, it is not possible or necessary to create a policy file for certificate templates that are configured to build the subject string from Active Directory. + +NOTE: The policy files get loaded when a certificate request gets processed, therefore it is not needed to restart the certification authority service after a policy file has been created or changed. + +=== Configuring rules for the private key + +You can specify the following parameters for the private key: + +|=== +|Parameter |Mandatory | Description + +|AuditOnly +|no +|Audit Mode. No certificate requests get denied but a message will get written into the Event Log when a certificate request violates the given policy. Helps sharpening the policy rules before applying a policy onto a productive system. Defaults to false. + +|KeyAlgorithm +|no +|Specifies the key algorithm the certificate request must use. At the moment, this can be "RSA" or "ECC" (which covers both ECDH and ECDSA). Defaults to "RSA". + +|MinimumKeyLength +|no +|Specifies the minimum key length the certificate request must use. Defaults to "0" (any key size is allowed). Note that though the Windows Default policy module also verifies this, this may become handy in a migration scenario where you publish the same template both on the old and new certification authority and plan to increase key size when switching to the new one whilst keeping the productive system unchanged. + +|MaximumKeyLength +|no +|Specifies the maximum key length the certificate request can use. Defaults to "0" (any key size is allowed). + +|=== + +=== Configuring rules for subject relative distinguished names (RDNs) + +Rules for subject RDNs get specified within a "SubjectRule" node under "Subject" section. + +NOTE: Any subject RDN that is not defined is considered forbidden and will result in any certificate request containing it getting denied. + +A "SubjectRule" can/must contain the following nodes: + +|=== +|Parameter |Mandatory |Description + +|Field +|*yes* +|Specifies the type of the field. See the below list for possible values. *Please be aware that this field is case-sensitive.* + +|Mandatory +|no +|Specifies if this field *must* (true) or *may* (false) appear in the certificate request presented. Defaults to "false". + +|MaxOccurrences +|no +|Specifies how often this field may appear within a certificate request. Should always be 1 for must subject RDN types. Defaults to 1. + +|MinLength +|no +|Specifies the minimum amount of characters the field must contain, to avoid empty RDNs being requested. Defaults to 1. Note that you also can define minimum lengths for parts or the entire field content via regular expressions in the AllowedPatterns directive. + +|MaxLength +|no +|Specifies the maximum amount of characters the field may contain. Defaults to 128. Note that link:https://www.gradenegger.eu/?p=2717[there is also an upper limit set by the certification authority^]. Also note that you also can define maximum lengths for parts or the entire field content via regular expressions in the AllowedPatterns directive. + +|AllowedPatterns +|*yes* +|For any field type except the iPAddress one, you can define one or more regular expressions of which the requested field content must match at least one of to get permitted. The node is required, so if you want to allow any content, simply configure "^.*$" as expression. For the iPAddress SAN type, you would specify a subnet in CIDR (e.g. 192.168.0.0/16) notation instead of a regular expression. To allow any IP Address, specify 0.0.0.0/0. + +|DisallowedPatterns +|no +|Specifies one or more regular expression (or CIDR subnet in the case of iPAddress type), of which the field must match at least one to get denied (even if an allow pattern has matched). + +|=== + +To define a policy for one or more subject Relative Distinguished Name (RDN) types, adjust the "field" to one of the following (as defined in link:https://www.itu.int/itu-t/recommendations/rec.aspx?rec=X.520[ITU-T X.520^] and link:https://datatracker.ietf.org/doc/html/rfc4519#section-2[RFC 4519^]). + +NOTE: Each RDN type can only be defined once in a policy definition file! + +The following RDN types are enabled/allowed by default on AD CS: + +* countryName +* commonName +* domainComponent +* emailAddress +* organizationName +* organizationalUnit +* localityName +* stateOrProvinceName + +The following RDNs can additionally be defined but must also explicitly be enabled in the certification authority configuration (by modifying the link:https://www.gradenegger.eu/?p=10183[SubjectTemplate^] Registry value): + +* givenName +* initials +* surname +* streetAddress +* title +* unstructuredName +* unstructuredAddress +* deviceSerialNumber + +It is also possible to enable any kind of RDNs in AD CS if the link:https://www.gradenegger.eu/?p=952[CRLF_REBUILD_MODIFIED_SUBJECT_ONLY^] flag is enabled. This should enable the following: + +* postalCode +* description +* postOfficeBox +* telephoneNumber +* any "unknown" (not identified by one of the above names) RDN can be specified by using it's object identifier. The OID it must be specified with an "OID." prefix, e.g. "OID.1.2.3.4.5". + +NOTE: Usually, it is recommended to avoid enabling the link:https://www.gradenegger.eu/?p=952[CRLF_REBUILD_MODIFIED_SUBJECT_ONLY^] flag, but when using this policy module, it should be fine as it allows fine-grained control about which RDN types are allowed and which not. + +NOTE: Please be aware that the SubjectTemplate value of the CA uses a different syntax for field type names. + +=== Configuring rules for Subject Alternative Names + +Rules for subject RDNs get specified within a "SubjectRule" node under "Subject" section. + +The "SubjectRule" configuration is already described above. + +To define a policy for one or more subject alternative name (SAN) type, adjust the "field" to one of the following (as defined in link:https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6[RFC 5280^] with the exception of the (Microsoft-)proprietary userPrincipalName). + +* dNSName +* iPAddress +* userPrincipalName +* rfc822Name +* uniformResourceIdentifier + +NOTE: For the iPAddress "Field", you would specify a subnet in CIDR (e.g. 192.168.0.0/16) notation instead of a regular expression. The module then evaluates if the speciified IP addresses reside in one of the speciified subnet. + +NOTE: Other SAN types are currently not implemented (yet). The ones currently implemented should be sufficient for most use cases. + +== Monitoring and Troubleshooting + +If a certificate request violates the defined policy, the certification authority will deny it with one of the below error codes and messages. It will log link:https://www.gradenegger.eu/?p=8544[Event with ID 53^]. The error code/message will also be handed over to the requesting client over the DCOM protocol as answer to the certificate request. + +The following error codes can be thrown by the policy module when a request was denied: + +|=== +|Message |Symbol |Description + +|The certificate has an invalid name. The name is not included in the permitted list or is explicitly excluded. +|CERT_E_INVALID_NAME +|Occurs if the request's subject oder subject alternative name violates the defined rules. + +|The public key does not meet the minimum size required by the specified certificate template. +|CERTSRV_E_KEY_LENGTH +|Occurs if the request's public key violates the defined rules for key algorithm or maximum key length. + +|The request subject name is invalid or too long. +|CERTSRV_E_BAD_REQUESTSUBJECT +|Occurs if the request's subject string cannot be interpreted by the policy module. + +|The parameter is incorrect. +|ERROR_INVALID_DATA +|Occurs if the policy module is unable to interpret the given policy file. + +|=== + +WARNING: Please be aware that if no policy file exists for a given certificate template, the request gets accepted as this would be the original behavior of the Windows Default policy module. + +=== Logs + +In addition to the certification authorities regular log entries, the policy module will also write a detailed log entry if a certificate request was denied due to a policy violation or failure. Find the logs under the "Application" Event Log with the "TameMyCerts" Event Source. + +|=== +|ID |Type |Description + +|1 +|Information +|Occurs if the Windows Default policy was successfully loaded and TameMyCerts is ready to process incoming requests. Occurs only if the certification authorities "LogLevel" is set to 4 or higher. + +|2 +|Error +|Occurs if the Windows Default policy was *not* successfully loaded (link:https://docs.microsoft.com/en-us/windows/win32/api/certpol/nf-certpol-icertpolicy-initialize[Initialize^] method failed). Will cause the CA service to not start. + +|3 +|Error +|Occurs if the Windows Default policy throws an exception on the link:https://docs.microsoft.com/en-us/windows/win32/api/certpol/nf-certpol-icertpolicy-verifyrequest[VerifyRequest^] method (the certificate request gets denied in this case). + +|4 +|Error +|Occurs if the Windows Default policy was *not* successfully unloaded (link:https://docs.microsoft.com/en-us/windows/win32/api/certpol/nf-certpol-icertpolicy-shutdown[ShutDown^] method failed.). + +|5 +|Warning +|Occurs if AuditOnly is enabled for a certificate template and a certificate request would get denied because of a policy violation. Contains a detailed information which kind of policy violation caused the request to get denied. + +|6 +|Warning +|Occurs if a certificate request was denied because of a policy violation. Contains a detailed information which kind of policy violation caused the request to get denied. + +|7 +|Warning +|Occurs if there is no policy configuration file defined for the certificate template used certificate request. The certificate request gets allowed in this case. + +|8 +|Error +|Occurs if the TameMyCerts policy module was unable to determine information about the request's certificate template from either the CA or the Active Directory. + +|9 +|Error +|Occurs it the TameMyCerts policy module is loaded on a standalone certification authority, which is unsupported at the moment. Will cause the CA service to not start. + +|10 +|Error +|Occurs if a certificate request was denied because because the policy file for the certificate template could not be interpreted. + +|11 +|Information +|Occurs if the Windows Default policy module denied a certificate request, thus the additional logic of TameMyCerts was not triggered at all for the given request. Occurs only if the certification authorities "LogLevel" is set to 4 or higher. + +|=== + +== Building + +Call the supplied build scripts from the Visual Studio Developer command prompt: + +* link:TameMyCerts/make_debug.cmd[make_debug.cmd] for a debug build (does not increment version bumber). +* link:TameMyCerts/make_release.cmd[make_release.cmd] for a release build (auto-increments version number). \ No newline at end of file diff --git a/TameMyCerts/AutoVersionIncrement.cs b/TameMyCerts/AutoVersionIncrement.cs new file mode 100644 index 0000000..6811440 --- /dev/null +++ b/TameMyCerts/AutoVersionIncrement.cs @@ -0,0 +1,13 @@ +// This code was automatically generated. Do not make any manual changes to it. + +using System.Reflection; + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision + +[assembly: AssemblyVersion("1.0.410.1186")] +[assembly: AssemblyFileVersion("1.0.410.1186")] diff --git a/TameMyCerts/AutoVersionIncrement.tt b/TameMyCerts/AutoVersionIncrement.tt new file mode 100644 index 0000000..af177cb --- /dev/null +++ b/TameMyCerts/AutoVersionIncrement.tt @@ -0,0 +1,35 @@ +<#@ template language="C#" #> +// This code was automatically generated. Do not make any manual changes to it. + +using System.Reflection; + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision + +[assembly: AssemblyVersion("1.0.<#= this.BuildNumber #>.<#= this.RevisionNumber #>")] +[assembly: AssemblyFileVersion("1.0.<#= this.BuildNumber #>.<#= this.RevisionNumber #>")] +<#+ + // Days that have passed since Jan 1, 2021 00:00:00 + int BuildNumber = (int)(DateTime.UtcNow - new DateTime( + 2021, + 1, + 1, + 0, + 0, + 0) + ).TotalDays; + + // Minutes that have passed since today 00:00:00 + int RevisionNumber = (int)(DateTime.UtcNow - new DateTime( + (DateTime.UtcNow.Year), + (DateTime.UtcNow.Month), + (DateTime.UtcNow.Day), + 0, + 0, + 0) + ).TotalMinutes; +#> \ No newline at end of file diff --git a/TameMyCerts/CERTCLILib.dll b/TameMyCerts/CERTCLILib.dll new file mode 100644 index 0000000000000000000000000000000000000000..4c5493c608340b68c05db48a52408534e8b7d5ea GIT binary patch literal 11264 zcmeHNeRLevb-y!jR$hNdShgt!J23LvaR^>zrCojO0m5omyFx~`g=Nb&A+c7{SYEmx z%I?axQV2`z6r6+e0h|QtCJ_;*Bq8Kbz(6T2I(6|$6AVdPC!r-E>I9ND#XxE6a8l^+ zzL|Y9l2yYW?LR3C_xIlK-uv#m_s-j$owYVx^FM#@gBitDVrj zNunN2(D8?EPgi%FCcR;ywvdROYHaPRaf~Kp0K(as7<4;t%w+yN(gXy0ehG?wmT0m3 z-yI!lyU-noyN%}qM5mo~j>#S>M2lu9-Xk%I{2o_7_>~^sShn_+!QWg3fKApqYk#N* z8jGdk5X>^S0ZjNpeGuY##n6z(qLnScQRZ5Kkz;)r;(5i0;vOoeKXf#ln@&FJHHkV` z5oxm>-y@Irt&eNYYR+X(q%qVO4uzYd9Kc6eV9)On1wM_lhVd3$-#}lvn8}Zn*dz7< zyKjN5eRL(}xOx=m+S=WTaXa|0QGXzwEex_VUOH0Ub_MD4nY^_oOo++3^WMuKe0*+% zybf6cnFsMf{seLW(gb0DyWZbAV+~zQ3$V6F;x!VB5^t7xr^IhcoT5L2=Tj1&mYSbS z{7-23Wk=?o+>K(EX#psztkGdQX|P=mwgxs`dS30=49Pz3Fu!EiIjmN$+vBXOmA0E5 z+fvEyaM%TseZygENkbM7fHh&ic4_;bw9NzKTuy+kLAPw%D~@fw^!$y(woA{2y6w4L z+E#&amq#RPc34Sz#vRWR)zlm?)*AhlTa6C`?~-_*#3v*k2XgPk;$yyT`buH(kJcPH<_>lP?aUWd;f&pMa!x>{xX zysptX4zn}k(`skNFy?djaT&*4s%#wd+$!^ZS05BO(0Fm4Kh>`j7bx}yVy;w- zgqPKdQI9PkZV-6u;Kfn8#Y)kj81E2JjCWY881HbgV!VUPcn6p94)%vlb%#c^ZoBxh z2q|`h_^Jpic36Bzv?_K~Oo_`B`>}Xdd{nVh;sw#67-yYQjI&;^7-!w17-#J=&e~<1 z^#quAxfsK_bSYb-GGij|BD zj88huK3P?EOyk;J#BRk3C6yq9BD8^Yorx<7bykeZE z%Q(-UDI4ecf?}Mf%Q(*$m5uY9c9`AyDziK9GS2!X$Mcx%{4W&atY1-#v;G&wIO|sx z#h5vJ%gIt7##9h-zsa)k%rq z3HCComlC6;_zhs0lpdN-DO|UDiRS}rr~$YLPoyb4sYZe4(bd2YAhMqt4L@vQ;9_bA zE~CqV%PDS{bO{ASBdwqpj7IE!4{#|xY$PZ^{gl9b9SumIAvqtSFzuv@)a-}mR@zUO z(j&kCWCHrAn1udGnv@Zypy?7vq4^aZm6~a2dc{e^S*o3sdeS%tp>YJEEv1MiG>&OP z9~A*;K7nUO&L;*%fW2ZGSQaEMp##DM9uhHNKnwt*q5|v^ z6Tn`4S7y&?-~mAfdz!!^;X#y)o40zC}00UwI7!^~%E-?-46?6{AF@Xm}40uQk04<{e%o`KHy~Y%< zVoU=M8iE?cJV-6%97sLnJjnTw6_AS{0mvH2TF51kk3gCrVMrS!3b_<=1tbCKhFk^N z0J$2{2iXeQ0r@y24Y432kQ^Nm1&rg68z47A_Cxmtj9-G>4*3e?F34fX*CCUTdm;Bh z?uR@C|05WmgdBxD1^FB3euD8i$Uj1U26++k666<Hg&5C;oDW$6xd^%-#*aXnAYn)gqz!T@Y?ot9K$4Iy@NSHoAbpr`g>1w8 zV~{lF7RC%Dhj{_x7-TPG9Q+S3-URt9<~KtQVtxp6JLX@(co_0^%i-9KY34U6I zA3*)M3oXa}=X%->EK@IV4{ZkSqbgIobuhm39L^PuBszh~IJR=r+pZ{`D|@8TcUG0epn+1U^Q81biIN?e+8oeGT}R zxUbaH6nz8uS9A~Xukj0PJsqQO0>4M!27aHu1ALmki#mQu&jA0KehBduW8ie+m!RV?JTjE`D9nZb@S ze!m_ZD_hh!R4x>$Yb-PD>Kvzjt60hu@@MGTJf||1P2(r=&P-{v+Y>EONMzHclDooA zmpBY<8obULDwFh(dGAUOd4kW>%N5D|SdJ3O&07*Z-KeB-IGd$hX{b=lW(KJ{VHL|= zRyk3~@5+pL+$b^2GK0!(?8VMX8OWYD+iR;Kd7(Y`tlDgSa?C1~?GO%Wp25Pjy(JV) z=8J`FHfQC_9b@I)cE4n1)c{`NiO<*4D)tt#nW1qzowIAsSeo3IL6amotWVXMf-8N6?7FozVN zu9k|aIuj)}I33<>;cQxatX?ZWjDKSx$G>}+ckW4-%6;W@d91{2hKbKzp_D1($ZoJo zrSyo!E{P5IV?b{r{WN4vM>hl$UYjL%JVZD754BknB}Wh+(l`sBZ5anfhh-R!ww_668% z?J8QO-Of?mlg^G=J9o;n&AVOIwD6WZB;*4<#by1sY+x6M)#zZwV!k4DxC=G@NtcbRD117q#9AS+C3;r zjgTJiN-mCe5JT&){i83{AnforjA;EzD7i1VzxN?1KMN^=(_TBZnHbv<|BVb&)9QStuKE2wOzY2O zg`6&Fkcy!H(NA65e|}{8;KdrPPP8RELeWGhm`FCY1)G{WL&5g;a5xwWb%YYpP&nQm zPT*f!HHt0f{cvNv&~qD@`T1^tl(DlebCdB~6NyMP{_gSR3gRWSsc3UN7!G$d2P5sRO~H67+!}0-hQsl;L}zPTJWSM}(aPC{IBzSb_1u(b zdq*l2PRab-_RX1JM<^7EHa7=Tsdy5VBvQEcq1lc8phgX|clF)}JzIC!0b9==kdutW zJEF;WG!$$LHz!mRTW2+KwMG}sy*1xSJsU?7k&c!muE|-$;hj+D*OuaZ;_m?l5&mWbW5iK!@K3q2s7B0)8p5DUy*JFvZ?0d4b%46@Jsu{PiK3Fj8j? zN9T#$)2W0xGSy+kFA5-fjbGH%hvau$LnAEZx2(Y&KNrcxNNxE%Q=nywS0XFU6q^AT zuSA%6)dKz1$^7bML3OgQI$2bm)XK>@8ZB@ytNjje-t;*9XM6uG;^NfniMX253p_Vn z?wwWtcEzhB+L@DYYQ&tMRraJn0tUfiDiQr|}e=fi*Af zH_`)mUvrE;=!>xA@%JicMtUA8X%xbLZHOJB2%d@213Yiz?{)CYYJqb2&%$ba6^Gu0 ztoGPO<^JQSHZA>NmOksGPj$aeL@3da%wu#$o*XZPr+H1~)(>Ai4R~s4qDJ^sg(!x< zNYP|B%F;KFy|bPS67pJXLH0fH>OrJItR99>mea#;*IZi<){e-PT;(W@%j`z*3qTn@ zCd976^|QM@jCDDzE7I@Fj@Z6e%A7ViF}T~DQ?-6(+(kl8d`|x7-0a@=B2EDrkD+H} iPi6LAcK$PcW_OMAfB)rlAZGuq+-uG6cjW(L2mTu*1?ma_ literal 0 HcmV?d00001 diff --git a/TameMyCerts/CERTPOLICYLib.dll b/TameMyCerts/CERTPOLICYLib.dll new file mode 100644 index 0000000000000000000000000000000000000000..afd38ec8f97d32b99000f1b97c0f621574fcd82d GIT binary patch literal 5120 zcmd^DU2Gd!6+U+yr*6`uX-amxB;8InDeO<}j$BwgMDJkck(Kz-Xdmjq3%?trU!8n!{2ftxZ+zpX z)6^TDyX#psy=>L%uCMRdy4R}fPF*jotm`$mVkZU$`X+wqkPk&a1Id4rPq^Gy2y$azF4`#RB({C7A9tTLhZpC?*jIt$=J z939&#jicg^<#nQ1$S4N>r4XO+?b|;10=6ml!g~$!Zky>u+X=7fm07^KWdVFpgMiS; zLX+_9stZF7L)!q3H4F%iEKxqhN>b;#VsLI6MQK$h`cWT|VBsei`Sma4Mc~c3+)5{m zM9N4d=QvoDs=)hEqKR)4y^r}e@|;-rJ*U3gWQz)l#&w(6T&F4soE}XqZ59idzXe`r z`+U{i0nY>M&{Br9lgu1kOwki6_W`gR2P`E4Cje~oS-=GV>v)f9pJh$Wz4T+ciHOHY!g|MiHC~pP}M7=9v!1x$yZY9mjmK z!*R?G7d@j*(z&)Sn$<4R`3`qYo1td{POW~itrP5b$gv+DcMR-7u-|18x}aaGVg2L|OOttC%W%fsw2)shqfH%nk zei=_AcZg2&^npmwS=yrnjna34`{@Q{AwLi%P2d?cA%7b$vw4bWWhp5`@&K>+c{-)N zCh6BGMMZH(Mtci3S@9OVKpEgs8W8tnw0A+*#Je)WeHrZmy#62_NI40P77?TLeGw7t z8xb5whkQia2L3?o10RSxz!B{p@G0#+@Q6klYjxlQkp+%u`%-d8O72NX1UonZI1M-h z7y+CIT%a9PY8(BzO>w|w@JXs5vu&CNWB~JkD}c`fOz0LcF9E&?dKvQy;7fo_z?T8n z0AGXdI%W$H#q-q%d`_O*r*UyaabiQjPvdfk(X%**7@mn|fG^P`_3Wd!DLnZ3*x1%@mnovqRdfTFs`iT(;`g zuC2lnw^$!)B0Z1w`+;DR^4}M zcA{AKZO?72+ukjwY&R*e-?F`?cw5qx0q1|S^Cd*inVTyIfcdV-Os!dJbGu`?P zXP2-u=f>We{mqu$^sy55kI#{vtM)5xnM!`i_6v5i>^Th|g`oADEx+KtQYXl}jw(_1 zU5}-!C?k4dkJnm<45wLg%a-(psI9(g)o=(Q^TsW!qvU*$WXnSh!p)SZRQdA(uWwyW z|M=nX-+y}0Uwm`Xx$Cd}_SY2q&8uJCdMbVYjUHWy!4rBdKj|!fL#(MP#s903jL=0GnGb0j@Gai19Ln-mJI4j1)=&+!HuC(yc)&An)w4M_* znaUaY%yim_n^Wd&Jee#^#q+6TDsC92kzUNq70k?3U^$!5Pv;CHo|#F_u%!{tr=?}i zFz1r_{Nk*MuN6UAd5g;6g?xH0X)MHN4I>#(=NFUlTrQc9FQ!wuSz|Vp%;bz9cz$MP zI*pKzOasBy0HH~0LeSG)TTFzWT=kgAZ7yUAnOq@_%@tAwPCgl*%cTp!HqEKI%tF4M zNj{xU&3sHImjyk4%(@?a1q3ayk3NxoccyVHmdFOr@;;{E^33i+*uFRlIVU8w#>SA9gzb}s?Gk|p}N z>K+z>P6KbzI_{z^+*@m)inyC8vbl}`&flY2TX=^4Y~dvc56vHsHM#%Y17WUfCx=0%RCxg zd2&1hH?!#Ewgp=c)`xpZ;=9S{0De#6UKg?&zO=1l?P@3kQ=Y{Jt)l)V*qQidC?Vbs zVpL$sFW6^gekJ(tN^jn21K(*}sa?8>9K-uKv{MzYz^?{BkG>(RqHIetuazJMSDf=| z*MK>$Bq1N}l}G2NYP^bAE^=<6c7AAwYCW}wZkwak)3M%TKXmnf2HE+?z7mgR0saf; CU5zFH literal 0 HcmV?d00001 diff --git a/TameMyCerts/Headers.cs b/TameMyCerts/Headers.cs new file mode 100644 index 0000000..9e720d8 --- /dev/null +++ b/TameMyCerts/Headers.cs @@ -0,0 +1,71 @@ +// Some constants that are defined in Windows SDK header files + +namespace TameMyCerts +{ + // Constants from CertCli.h + public static class CertCli + { + // See also https://docs.microsoft.com/en-us/windows/win32/api/certcli/nf-certcli-icertrequest-submit + public const int CR_IN_PKCS10 = 0x100; + public const int CR_IN_KEYGEN = 0x200; + public const int CR_IN_PKCS7 = 0x300; + public const int CR_IN_CMC = 0x400; + public const int CR_IN_FULLRESPONSE = 0x40000; + } + + // Constants from CertSrv.h + public static class CertSrv + { + // See also https://docs.microsoft.com/en-us/windows/win32/api/certpol/nf-certpol-icertpolicy-verifyrequest + public const int VR_PENDING = 0; + public const int VR_INSTANT_OK = 1; + public const int VR_INSTANT_BAD = 2; + + public const int CERTLOG_MINIMAL = 0; + public const int CERTLOG_TERSE = 1; + public const int CERTLOG_ERROR = 2; + public const int CERTLOG_WARNING = 3; + public const int CERTLOG_VERBOSE = 4; + public const int CERTLOG_EXHAUSTIVE = 5; + + public const int PROPTYPE_LONG = 1; + public const int PROPTYPE_DATE = 2; + public const int PROPTYPE_BINARY = 3; + public const int PROPTYPE_STRING = 4; + public const int PROPTYPE_ANSI = 5; + + public const int ENUM_ENTERPRISE_ROOTCA = 0; + public const int ENUM_ENTERPRISE_SUBCA = 1; + public const int ENUM_STANDALONE_ROOTCA = 3; + public const int ENUM_STANDALONE_SUBCA = 4; + } + + // Constants from CertCa.h + public static class CertCa + { + // The enrolling application must supply the subject name. + public const int CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT = 1; + } + + // Constants from WinError.h + public static class WinError + { + // The operation completed successfully. + public const int ERROR_SUCCESS = 0; + + // An internal error occurred. + public const int NTE_FAIL = unchecked((int) 0x80090020); + + // The request subject name is invalid or too long. + public const int CERTSRV_E_BAD_REQUESTSUBJECT = unchecked((int) 0x80094001); + + // The requested certificate template is not supported by this CA. + public const int CERTSRV_E_UNSUPPORTED_CERT_TYPE = unchecked((int) 0x80094800); + + // The public key does not meet the minimum size required by the specified certificate template. + public const int CERTSRV_E_KEY_LENGTH = unchecked((int) 0x80094811); + + // The certificate has an invalid name. The name is not included in the permitted list or is explicitly excluded. + public const int CERT_E_INVALID_NAME = unchecked((int) 0x800B0114); + } +} \ No newline at end of file diff --git a/TameMyCerts/IPAddressExtensions.cs b/TameMyCerts/IPAddressExtensions.cs new file mode 100644 index 0000000..15656be --- /dev/null +++ b/TameMyCerts/IPAddressExtensions.cs @@ -0,0 +1,91 @@ +// See https://docs.microsoft.com/en-us/archive/blogs/knom/ip-address-calculations-with-c-subnetmasks-networks + +using System; +using System.Net; + +namespace TameMyCerts +{ + public static class IPAddressExtensions + { + + public static IPAddress GetIpFromString(this string ipInput) + { + var returnVal = IPAddress.Any; + try + { + returnVal = IPAddress.Parse(ipInput); + } + catch + { + } + + return returnVal; + } + + public static CidrMask GetCidrMask(this string cidrInput) + { + var returnVal = new CidrMask(); + var parts = cidrInput.Split('/'); + returnVal.address = BitConverter.ToInt32(IPAddress.Parse(parts[0]).GetAddressBytes(), 0); + returnVal.mask = IPAddress.HostToNetworkOrder(-1 << (32 - int.Parse(parts[1]))); + return returnVal; + } + + public static bool IsIp(this string ipInput) + { + return !IPAddress.Any.Equals(ipInput.GetIpFromString()); + } + + public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask) + { + var ipAddressBytes = address.GetAddressBytes(); + var subnetMaskBytes = subnetMask.GetAddressBytes(); + if (ipAddressBytes.Length != subnetMaskBytes.Length) + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + var broadcastAddress = new byte[ipAddressBytes.Length]; + for (var i = 0; i < broadcastAddress.Length; i++) + broadcastAddress[i] = (byte) (ipAddressBytes[i] | (subnetMaskBytes[i] ^ 255)); + return new IPAddress(broadcastAddress); + } + + public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask) + { + var ipAddressBytes = address.GetAddressBytes(); + var subnetMaskBytes = subnetMask.GetAddressBytes(); + if (ipAddressBytes.Length != subnetMaskBytes.Length) + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + var broadcastAddress = new byte[ipAddressBytes.Length]; + for (var i = 0; i < broadcastAddress.Length; i++) + broadcastAddress[i] = (byte) (ipAddressBytes[i] & subnetMaskBytes[i]); + return new IPAddress(broadcastAddress); + } + + public static bool IsInSameSubnet(this string address1, string address2, string subnetMask) + { + var network1 = address1.GetIpFromString(); + var network2 = address2.GetIpFromString(); + var subnet1 = subnetMask.GetIpFromString(); + return network1.IsInSameSubnet(network2, subnet1); + } + + public static bool IsInSameSubnet(this IPAddress address1, IPAddress address2, IPAddress subnetMask) + { + var network1 = address1.GetNetworkAddress(subnetMask); + var network2 = address2.GetNetworkAddress(subnetMask); + return network1.Equals(network2); + } + + public static bool IsInRange(this IPAddress address, string subnetMask) + { + var cidrMask = subnetMask.GetCidrMask(); + var ipAddress = BitConverter.ToInt32(address.GetAddressBytes(), 0); + return (ipAddress & cidrMask.mask) == (cidrMask.address & cidrMask.mask); + } + } + + public class CidrMask + { + public int address; + public int mask; + } +} \ No newline at end of file diff --git a/TameMyCerts/LocalizedStrings.Designer.cs b/TameMyCerts/LocalizedStrings.Designer.cs new file mode 100644 index 0000000..52e5a2d --- /dev/null +++ b/TameMyCerts/LocalizedStrings.Designer.cs @@ -0,0 +1,347 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TameMyCerts { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class LocalizedStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal LocalizedStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TameMyCerts.LocalizedStrings", typeof(LocalizedStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The {0} policy module currently does not support standalone certification authorities.. + /// + internal static string Events_MODULE_NOT_SUPPORTED { + get { + return ResourceManager.GetString("Events_MODULE_NOT_SUPPORTED", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error initializing Windows Default policy module: + ///{0}. + /// + internal static string Events_PDEF_FAIL_INIT { + get { + return ResourceManager.GetString("Events_PDEF_FAIL_INIT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shutting down Windows Default policy module failed: + ///{0}. + /// + internal static string Events_PDEF_FAIL_SHUTDOWN { + get { + return ResourceManager.GetString("Events_PDEF_FAIL_SHUTDOWN", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Windows Default policy module was unable to verify request {0}: + ///{1}. + /// + internal static string Events_PDEF_FAIL_VERIFY { + get { + return ResourceManager.GetString("Events_PDEF_FAIL_VERIFY", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request {0} was denied by the Windows Default policy module.. + /// + internal static string Events_PDEF_REQUEST_DENIED { + get { + return ResourceManager.GetString("Events_PDEF_REQUEST_DENIED", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} policy module version {1} is ready to process incoming certificate requests.. + /// + internal static string Events_PDEF_SUCCESS_INIT { + get { + return ResourceManager.GetString("Events_PDEF_SUCCESS_INIT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Certificate template {0} used for Request {1} is configured to build subject from Active Directory, skipping.. + /// + internal static string Events_POLICY_NOT_APPLICABLE { + get { + return ResourceManager.GetString("Events_POLICY_NOT_APPLICABLE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to find policy file for {0}. Request {1} will get issued. Expected policy file name: "{2}". + /// + internal static string Events_POLICY_NOT_FOUND { + get { + return ResourceManager.GetString("Events_POLICY_NOT_FOUND", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request {0} for {1} was denied because: + ///{2}. + /// + internal static string Events_REQUEST_DENIED { + get { + return ResourceManager.GetString("Events_REQUEST_DENIED", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Audit mode is enabled for {1}. Request {0} would get denied because: + ///{2}. + /// + internal static string Events_REQUEST_DENIED_AUDIT { + get { + return ResourceManager.GetString("Events_REQUEST_DENIED_AUDIT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to interpret policy from {0}. Request {1} will get denied.. + /// + internal static string Events_REQUEST_DENIED_NO_POLICY { + get { + return ResourceManager.GetString("Events_REQUEST_DENIED_NO_POLICY", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No certificate template information found for request {0}. The request will get denied.. + /// + internal static string Events_REQUEST_DENIED_NO_TEMPLATE_INFO { + get { + return ResourceManager.GetString("Events_REQUEST_DENIED_NO_TEMPLATE_INFO", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value "{0}" does match the disallowed pattern {1} for the {2} field.. + /// + internal static string ReqVal_Disallow_Match { + get { + return ResourceManager.GetString("ReqVal_Disallow_Match", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to extract inner PKCS#10 request from given CMC certificate request.. + /// + internal static string ReqVal_Err_Extract_From_Cmc { + get { + return ResourceManager.GetString("ReqVal_Err_Extract_From_Cmc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to extract inner PKCS#10 request from given PKCS#7 certificate request.. + /// + internal static string ReqVal_Err_Extract_From_Pkcs7 { + get { + return ResourceManager.GetString("ReqVal_Err_Extract_From_Pkcs7", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to parse the given certificate request. Request type was {0}.. + /// + internal static string ReqVal_Err_Parse_Request { + get { + return ResourceManager.GetString("ReqVal_Err_Parse_Request", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to parse the given subject distinguished name: {0}.. + /// + internal static string ReqVal_Err_Parse_SubjectDn { + get { + return ResourceManager.GetString("ReqVal_Err_Parse_SubjectDn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to match pattern "{0}" with value "{1}" for the {2} field.. + /// + internal static string ReqVal_Err_Regex { + get { + return ResourceManager.GetString("ReqVal_Err_Regex", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} field was found {1} times, but is allowed only {2} times.. + /// + internal static string ReqVal_Field_Count_Mismatch { + get { + return ResourceManager.GetString("ReqVal_Field_Count_Mismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The mandatory {0} field was not found in the request.. + /// + internal static string ReqVal_Field_Missing { + get { + return ResourceManager.GetString("ReqVal_Field_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} field is not allowed.. + /// + internal static string ReqVal_Field_Not_Allowed { + get { + return ResourceManager.GetString("ReqVal_Field_Not_Allowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No allowed patterns are defined for the {0} field.. + /// + internal static string ReqVal_Field_Not_Defined { + get { + return ResourceManager.GetString("ReqVal_Field_Not_Defined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value "{0}" for the {1} field exceeds the maximum allowed length of {2} characters.. + /// + internal static string ReqVal_Field_Too_Long { + get { + return ResourceManager.GetString("ReqVal_Field_Too_Long", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value "{0}" for the {1} field deceeds of the minimum required length of {2} characters.. + /// + internal static string ReqVal_Field_Too_Short { + get { + return ResourceManager.GetString("ReqVal_Field_Too_Short", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The certificate request uses an {0} key pair, but must use an {1} key pair.. + /// + internal static string ReqVal_Key_Pair_Mismatch { + get { + return ResourceManager.GetString("ReqVal_Key_Pair_Mismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Key length of {0} Bits is more than the allowed maximum length of {1} Bits.. + /// + internal static string ReqVal_Key_Too_Large { + get { + return ResourceManager.GetString("ReqVal_Key_Too_Large", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Key length of {0} Bits is less than the required minimum length of {1} Bits.. + /// + internal static string ReqVal_Key_Too_Small { + get { + return ResourceManager.GetString("ReqVal_Key_Too_Small", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value "{0}" does not match any of the allowed patterns for the {1} field.. + /// + internal static string ReqVal_No_Match { + get { + return ResourceManager.GetString("ReqVal_No_Match", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The certificate request contains the unsupported Subject Directory Attributes extension.. + /// + internal static string ReqVal_Unsupported_Extension_Dir_Attrs { + get { + return ResourceManager.GetString("ReqVal_Unsupported_Extension_Dir_Attrs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The certificate request contains an unsupported Subject Alternative Name type with OID {0}.. + /// + internal static string ReqVal_Unsupported_San_Type { + get { + return ResourceManager.GetString("ReqVal_Unsupported_San_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown. + /// + internal static string Unknown { + get { + return ResourceManager.GetString("Unknown", resourceCulture); + } + } + } +} diff --git a/TameMyCerts/LocalizedStrings.resx b/TameMyCerts/LocalizedStrings.resx new file mode 100644 index 0000000..e09b76f --- /dev/null +++ b/TameMyCerts/LocalizedStrings.resx @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The {0} policy module currently does not support standalone certification authorities. + + + Error initializing Windows Default policy module: +{0} + + + Shutting down Windows Default policy module failed: +{0} + + + Windows Default policy module was unable to verify request {0}: +{1} + + + {0} policy module version {1} is ready to process incoming certificate requests. + + + Certificate template {0} used for Request {1} is configured to build subject from Active Directory, skipping. + + + Unable to find policy file for {0}. Request {1} will get issued. Expected policy file name: "{2}" + + + Request {0} for {1} was denied because: +{2} + + + Audit mode is enabled for {1}. Request {0} would get denied because: +{2} + + + Unable to interpret policy from {0}. Request {1} will get denied. + + + No certificate template information found for request {0}. The request will get denied. + + + Unable to extract inner PKCS#10 request from given CMC certificate request. + + + Unable to extract inner PKCS#10 request from given PKCS#7 certificate request. + + + Unable to parse the given certificate request. Request type was {0}. + + + The certificate request uses an {0} key pair, but must use an {1} key pair. + + + Key length of {0} Bits is less than the required minimum length of {1} Bits. + + + Key length of {0} Bits is more than the allowed maximum length of {1} Bits. + + + Unable to parse the given subject distinguished name: {0}. + + + The certificate request contains an unsupported Subject Alternative Name type with OID {0}. + + + The certificate request contains the unsupported Subject Directory Attributes extension. + + + The mandatory {0} field was not found in the request. + + + The {0} field was found {1} times, but is allowed only {2} times. + + + The {0} field is not allowed. + + + The value "{0}" for the {1} field exceeds the maximum allowed length of {2} characters. + + + No allowed patterns are defined for the {0} field. + + + Unable to match pattern "{0}" with value "{1}" for the {2} field. + + + The value "{0}" does not match any of the allowed patterns for the {1} field. + + + The value "{0}" does match the disallowed pattern {1} for the {2} field. + + + unknown + + + The value "{0}" for the {1} field deceeds of the minimum required length of {2} characters. + + + Request {0} was denied by the Windows Default policy module. + + \ No newline at end of file diff --git a/TameMyCerts/Logger.cs b/TameMyCerts/Logger.cs new file mode 100644 index 0000000..0feb403 --- /dev/null +++ b/TameMyCerts/Logger.cs @@ -0,0 +1,165 @@ +// Copyright 2021 Uwe Gradenegger + +// 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. + +using System.Diagnostics; +using System.Security; + +namespace TameMyCerts +{ + public class Logger + { + private readonly EventLog _eventLog; + private readonly int _logLevel; + + public Logger(string eventSource, int logLevel = CertSrv.CERTLOG_WARNING) + { + _logLevel = logLevel; + _eventLog = new EventLog("Application") + { + Source = CreateEventSource(eventSource) + }; + } + + public void Log(Event logEvent, params object[] args) + { + if (_logLevel >= logEvent.LogLevel) + _eventLog.WriteEntry( + string.Format(logEvent.MessageText, args), + logEvent.Type, + logEvent.ID); + } + + private static string CreateEventSource(string currentAppName) + { + var eventSource = currentAppName; + + try + { + var sourceExists = EventLog.SourceExists(eventSource); + if (!sourceExists) + EventLog.CreateEventSource(eventSource, "Application"); + } + catch (SecurityException) + { + eventSource = "Application"; + } + + return eventSource; + } + } + + public static class Events + { + public static Event PDEF_SUCCESS_INIT = new Event + { + ID = 1, + LogLevel = CertSrv.CERTLOG_VERBOSE, + MessageText = LocalizedStrings.Events_PDEF_SUCCESS_INIT + }; + + public static Event PDEF_FAIL_INIT = new Event + { + ID = 2, + LogLevel = CertSrv.CERTLOG_ERROR, + Type = EventLogEntryType.Error, + MessageText = LocalizedStrings.Events_PDEF_FAIL_INIT + }; + + public static Event PDEF_FAIL_VERIFY = new Event + { + ID = 3, + LogLevel = CertSrv.CERTLOG_ERROR, + Type = EventLogEntryType.Error, + MessageText = LocalizedStrings.Events_PDEF_FAIL_VERIFY + }; + + public static Event PDEF_FAIL_SHUTDOWN = new Event + { + ID = 4, + LogLevel = CertSrv.CERTLOG_ERROR, + Type = EventLogEntryType.Error, + MessageText = LocalizedStrings.Events_PDEF_FAIL_SHUTDOWN + }; + + public static Event REQUEST_DENIED_AUDIT = new Event + { + ID = 5, + LogLevel = CertSrv.CERTLOG_MINIMAL, + Type = EventLogEntryType.Warning, + MessageText = LocalizedStrings.Events_REQUEST_DENIED_AUDIT + }; + + public static Event REQUEST_DENIED = new Event + { + ID = 6, + Type = EventLogEntryType.Warning, + MessageText = LocalizedStrings.Events_REQUEST_DENIED + }; + + public static Event POLICY_NOT_FOUND = new Event + { + ID = 7, + Type = EventLogEntryType.Warning, + MessageText = LocalizedStrings.Events_POLICY_NOT_FOUND + }; + + public static Event POLICY_NOT_APPLICABLE = new Event + { + ID = 8, + LogLevel = CertSrv.CERTLOG_EXHAUSTIVE, + Type = EventLogEntryType.Warning, + MessageText = LocalizedStrings.Events_POLICY_NOT_APPLICABLE + }; + + public static Event MODULE_NOT_SUPPORTED = new Event + { + ID = 9, + LogLevel = CertSrv.CERTLOG_ERROR, + Type = EventLogEntryType.Error, + MessageText = LocalizedStrings.Events_MODULE_NOT_SUPPORTED + }; + + public static Event REQUEST_DENIED_NO_TEMPLATE_INFO = new Event + { + ID = 10, + LogLevel = CertSrv.CERTLOG_ERROR, + Type = EventLogEntryType.Error, + MessageText = LocalizedStrings.Events_REQUEST_DENIED_NO_TEMPLATE_INFO + }; + + public static Event REQUEST_DENIED_NO_POLICY = new Event + { + ID = 10, + LogLevel = CertSrv.CERTLOG_ERROR, + Type = EventLogEntryType.Error, + MessageText = LocalizedStrings.Events_REQUEST_DENIED_NO_POLICY + }; + + public static Event PDEF_REQUEST_DENIED = new Event + { + ID = 11, + LogLevel = CertSrv.CERTLOG_VERBOSE, + MessageText = LocalizedStrings.Events_PDEF_REQUEST_DENIED + }; + } + + public class Event + { + public int ID { get; set; } + public int LogLevel { get; set; } = CertSrv.CERTLOG_WARNING; + + public EventLogEntryType Type { get; set; } = EventLogEntryType.Information; + public string MessageText { get; set; } + } +} \ No newline at end of file diff --git a/TameMyCerts/Policy.cs b/TameMyCerts/Policy.cs new file mode 100644 index 0000000..8f30f28 --- /dev/null +++ b/TameMyCerts/Policy.cs @@ -0,0 +1,472 @@ +// Copyright 2021 Uwe Gradenegger + +// 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. + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using CERTCLILib; +using CERTPOLICYLib; +using Microsoft.Win32; + +namespace TameMyCerts +{ + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [ProgId("TameMyCerts.Policy")] + [Guid("432413c6-2e86-4667-9697-c1e038877ef9")] // must be distinct from PolicyManage Class + public class Policy : ICertPolicy2 + { + private readonly string _appName; + private readonly string _appVersion; + private Logger _logger; + private string _policyDirectory; + private TemplateInfo _templateInfo = new TemplateInfo(); + private dynamic _windowsDefaultPolicyModule; + + public Policy() + { + var assembly = Assembly.GetExecutingAssembly(); + + _appName = ((AssemblyTitleAttribute) assembly.GetCustomAttribute( + typeof(AssemblyTitleAttribute))).Title; + + _appVersion = ((AssemblyFileVersionAttribute) assembly.GetCustomAttribute( + typeof(AssemblyFileVersionAttribute))).Version; + } + + #region ICertPolicy2 Members + + public CCertManagePolicyModule GetManageModule() + { + return new PolicyManage(); + } + + #endregion + + #region ICertPolicy Members + + public string GetDescription() + { + return _appName; + } + + public void Initialize(string strConfig) + { + // Load Settings from Registry + var configRoot = + $"HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{strConfig}"; + + // Initialize Event Log + var logLevel = (int) Registry.GetValue( + configRoot, + "LogLevel", + CertSrv.CERTLOG_WARNING + ); + + _logger = new Logger(_appName, logLevel); + + // Prevent the usage on a standalone CA + var caType = (int) Registry.GetValue( + configRoot, + "CAType", + CertSrv.ENUM_STANDALONE_ROOTCA + ); + + if (!(caType == CertSrv.ENUM_ENTERPRISE_ROOTCA || caType == CertSrv.ENUM_ENTERPRISE_SUBCA)) + { + _logger.Log(Events.MODULE_NOT_SUPPORTED, _appName); + + // Abort loading the policy module. + throw new NotSupportedException(); + } + + // Load Settings from Registry + _policyDirectory = (string) Registry.GetValue( + $"{configRoot}\\PolicyModules\\{_appName}.Policy", + "PolicyDirectory", + Path.GetTempPath() + ); + + // Load the Windows default default policy module + try + { + var windowsDefaultPolicyModuleType = Type.GetTypeFromCLSID( + new Guid("3B6654D0-C2C8-11D2-B313-00C04F79DC72"), + true + ); + + _windowsDefaultPolicyModule = Activator.CreateInstance(windowsDefaultPolicyModuleType); + + _windowsDefaultPolicyModule.GetType().InvokeMember( + "Initialize", + BindingFlags.InvokeMethod, + null, + _windowsDefaultPolicyModule, + new object[] {strConfig} + ); + + _logger.Log(Events.PDEF_SUCCESS_INIT, _appName, _appVersion); + } + catch (Exception ex) + { + _logger.Log(Events.PDEF_FAIL_INIT, ex); + + // Abort loading the policy module. This will cause the certification authority service to fail + throw; + } + } + + public int VerifyRequest(string strConfig, int context, int bNewRequest, int flags) + { + int result; + + var certServerPolicy = new CCertServerPolicy(); + + certServerPolicy.SetContext(context); + + var requestId = GetLongRequestProperty(ref certServerPolicy, "RequestId"); + + // Hand the request over to the default policy module + try + { + result = (int) _windowsDefaultPolicyModule.GetType().InvokeMember( + "VerifyRequest", + BindingFlags.InvokeMethod, + null, + _windowsDefaultPolicyModule, + new object[] {strConfig, context, bNewRequest, flags} + ); + } + catch (Exception ex) + { + if (ex.InnerException is COMException) + { + _logger.Log(Events.PDEF_FAIL_VERIFY, requestId, ex); + + // Some reasons to deny a request come in form of COM Exceptions + // If so, we're done here and hand the HResult back to the CA process + return ex.InnerException.HResult; + } + + _logger.Log(Events.PDEF_FAIL_VERIFY, requestId, ex); + + return CertSrv.VR_INSTANT_BAD; + } + + // We don't question if the default policy module decided to deny the request + // Note that a policy module can also return any HResult code as defined in winerr.h + if (!(result == CertSrv.VR_PENDING || result == CertSrv.VR_INSTANT_OK)) + { + _logger.Log(Events.PDEF_REQUEST_DENIED, requestId); + return result; + } + + // No need to check every request twice. If this request was permitted by a certificate manager, we are fine with it + if (bNewRequest == 0) + return result; + + // At this point, the certificate is completely constructed and ready to be issued, if we allow it + + #region Additional Checks + + // This retrieves the Oid of the Certificate Template which was used to build the Certificate + var templateOid = GetStringCertificateProperty(ref certServerPolicy, "CertificateTemplate"); + + // Deny the request if no template info can be found (this should never happen though) + if (null == templateOid) + { + _logger.Log(Events.REQUEST_DENIED_NO_TEMPLATE_INFO, requestId); + + return WinError.CERTSRV_E_UNSUPPORTED_CERT_TYPE; + } + + var templateInfo = _templateInfo.GetTemplate(templateOid); + + // Deny the request if no template info can be found in local cache (this should never happen though) + if (null == templateInfo) + { + _logger.Log(Events.REQUEST_DENIED_NO_TEMPLATE_INFO, requestId); + + return WinError.CERTSRV_E_UNSUPPORTED_CERT_TYPE; + } + + // Don't bother with templates that are configured to build subject from AD + if (!templateInfo.EnrolleeSuppliesSubject) + { + _logger.Log(Events.POLICY_NOT_APPLICABLE, templateInfo.Name, requestId); + + return result; + } + + // Finally... here we will do our additional checks + var policyFile = Path.Combine(_policyDirectory, $"{templateInfo.Name}.xml"); + + // Issue the certificate of no policy is defined + if (!File.Exists(policyFile)) + { + _logger.Log(Events.POLICY_NOT_FOUND, templateInfo.Name, requestId, policyFile); + + return result; + } + + var requestValidator = new CertificateRequestValidator(); + var requestPolicy = requestValidator.LoadFromFile(policyFile); + + // Deny the request if unable to parse policy file + if (null == requestPolicy) + { + _logger.Log(Events.REQUEST_DENIED_NO_POLICY, policyFile, requestId); + + return WinError.NTE_FAIL; + } + + var request = GetBinaryRequestProperty(ref certServerPolicy, "RawRequest"); + var requestType = GetLongRequestProperty(ref certServerPolicy, "RequestType") ^ CertCli.CR_IN_FULLRESPONSE; + + // Verify the Certificate request against policy + var validationResult = + requestValidator.VerifyRequest(Convert.ToBase64String(request), requestPolicy, requestType); + + // No reason to deny the request, let's issue the certificate + if (validationResult.Success) return result; + + // Also issue the certificate when the request policy is set to AuditOnly + if (validationResult.AuditOnly) + { + _logger.Log(Events.REQUEST_DENIED_AUDIT, requestId, templateInfo.Name, + string.Join("\n", validationResult.Description)); + + return result; + } + + // Deny the request if validation was not successful + _logger.Log(Events.REQUEST_DENIED, requestId, templateInfo.Name, + string.Join("\n", validationResult.Description)); + + // Return the result code handed over by the RequestValidator class + return validationResult.StatusCode; + + #endregion + } + + public void ShutDown() + { + try + { + _windowsDefaultPolicyModule.GetType().InvokeMember( + "Shutdown", + BindingFlags.InvokeMethod, + null, + _windowsDefaultPolicyModule, + null + ); + } + catch (Exception ex) + { + _logger.Log(Events.PDEF_FAIL_SHUTDOWN, ex); + } + } + + #endregion + + #region Property retrieval functions + + [DllImport(@"oleaut32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)] + private static extern int VariantClear(IntPtr pvarg); + + private DateTime GetDateCertificateProperty(ref CCertServerPolicy server, string name) + { + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + server.GetCertificateProperty(name, CertSrv.PROPTYPE_DATE, variantObjectPtr); + var result = (DateTime) Marshal.GetObjectForNativeVariant(variantObjectPtr); + return result; + } + catch + { + return new DateTime(); + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private string GetStringCertificateProperty(ref CCertServerPolicy server, string name) + { + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + server.GetCertificateProperty(name, CertSrv.PROPTYPE_STRING, variantObjectPtr); + var result = (string) Marshal.GetObjectForNativeVariant(variantObjectPtr); + return result; + } + catch + { + return null; + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private int GetLongCertificateProperty(ref CCertServerPolicy server, string name) + { + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + server.GetCertificateProperty(name, CertSrv.PROPTYPE_LONG, variantObjectPtr); + var result = (int) Marshal.GetObjectForNativeVariant(variantObjectPtr); + return result; + } + catch + { + return 0; + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private byte[] GetBinaryCertificateProperty(ref CCertServerPolicy server, string name) + { + // https://blogs.msdn.microsoft.com/alejacma/2008/08/04/how-to-modify-an-interop-assembly-to-change-the-return-type-of-a-method-vb-net/ + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + // Get VARIANT containing certificate bytes + // Read ANSI BSTR information from the VARIANT as we know RawCertificate property is ANSI BSTR. + server.GetCertificateProperty(name, CertSrv.PROPTYPE_BINARY, variantObjectPtr); + var bstrPtr = Marshal.ReadIntPtr(variantObjectPtr, 8); + var bstrLen = Marshal.ReadInt32(bstrPtr, -4); + var result = new byte[bstrLen]; + Marshal.Copy(bstrPtr, result, 0, bstrLen); + + return result; + } + catch + { + return null; + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private DateTime GetDateRequestProperty(ref CCertServerPolicy server, string name) + { + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + server.GetRequestProperty(name, CertSrv.PROPTYPE_DATE, variantObjectPtr); + var result = (DateTime) Marshal.GetObjectForNativeVariant(variantObjectPtr); + return result; + } + catch + { + return new DateTime(); + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private string GetStringRequestProperty(ref CCertServerPolicy server, string name) + { + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + server.GetRequestProperty(name, CertSrv.PROPTYPE_STRING, variantObjectPtr); + var result = (string) Marshal.GetObjectForNativeVariant(variantObjectPtr); + return result; + } + catch + { + return null; + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private int GetLongRequestProperty(ref CCertServerPolicy server, string name) + { + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + server.GetRequestProperty(name, CertSrv.PROPTYPE_LONG, variantObjectPtr); + var result = (int) Marshal.GetObjectForNativeVariant(variantObjectPtr); + return result; + } + catch + { + return 0; + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + private byte[] GetBinaryRequestProperty(ref CCertServerPolicy server, string name) + { + // https://blogs.msdn.microsoft.com/alejacma/2008/08/04/how-to-modify-an-interop-assembly-to-change-the-return-type-of-a-method-vb-net/ + var variantObjectPtr = Marshal.AllocHGlobal(2048); + + try + { + // Get VARIANT containing certificate bytes + // Read ANSI BSTR information from the VARIANT as we know RawCertificate property is ANSI BSTR. + server.GetRequestProperty(name, CertSrv.PROPTYPE_BINARY, variantObjectPtr); + var bstrPtr = Marshal.ReadIntPtr(variantObjectPtr, 8); + var bstrLen = Marshal.ReadInt32(bstrPtr, -4); + var result = new byte[bstrLen]; + Marshal.Copy(bstrPtr, result, 0, bstrLen); + return result; + } + catch + { + return null; + } + finally + { + VariantClear(variantObjectPtr); + Marshal.FreeHGlobal(variantObjectPtr); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/TameMyCerts/PolicyManage.cs b/TameMyCerts/PolicyManage.cs new file mode 100644 index 0000000..d26e692 --- /dev/null +++ b/TameMyCerts/PolicyManage.cs @@ -0,0 +1,77 @@ +// Copyright 2021 Uwe Gradenegger + +// 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. + +using System.Reflection; +using System.Runtime.InteropServices; +using CERTPOLICYLib; + +namespace TameMyCerts +{ + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [ProgId("TameMyCerts.PolicyManage")] + [Guid("f24389a5-97a6-40c1-a1c6-aefd273fb634")] // must be distinct from Policy Class + public class PolicyManage : CCertManagePolicyModule + { + #region Constructors + + #endregion + + #region ICertManageModule Members + + public void Configure(string strConfig, string strStorageLocation, int flags) + { + // This method is intended for future functionality. + } + + public object GetProperty(string strConfig, string strStorageLocation, string strPropertyName, int flags) + { + var assembly = Assembly.GetExecutingAssembly(); + + switch (strPropertyName) // Each of these is required + { + case "Name": + return ((AssemblyTitleAttribute)assembly.GetCustomAttribute( + typeof(AssemblyTitleAttribute))).Title; + + case "Description": + return ((AssemblyDescriptionAttribute)assembly.GetCustomAttribute( + typeof(AssemblyDescriptionAttribute))).Description; + + case "Copyright": + return ((AssemblyCopyrightAttribute)assembly.GetCustomAttribute( + typeof(AssemblyCopyrightAttribute))).Copyright; + + case "File Version": + return ((AssemblyFileVersionAttribute)assembly.GetCustomAttribute( + typeof(AssemblyFileVersionAttribute))).Version; + + case "Product Version": + return ((AssemblyVersionAttribute)assembly.GetCustomAttribute( + typeof(AssemblyVersionAttribute))).Version; + + default: + return "Unknown Property: " + strPropertyName; + } + } + + public void SetProperty(string strConfig, string strStorageLocation, string strPropertyName, int flags, + ref object pvarProperty) + { + // This method is intended for future functionality. + } + + #endregion + } +} \ No newline at end of file diff --git a/TameMyCerts/Properties/AssemblyInfo.cs b/TameMyCerts/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..20f9287 --- /dev/null +++ b/TameMyCerts/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TameMyCerts")] +[assembly: AssemblyDescription("A policy module that allows to define rules for certificate templates having the enrollee supply the subject information.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TameMyCerts")] +[assembly: AssemblyCopyright("Copyright © 2021 Uwe Gradenegger")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bb35a67e-8e22-48c3-b3f8-e852161acb59")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +//[assembly: AssemblyVersion("1.0.0.0")] +//[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TameMyCerts/RequestValidator.cs b/TameMyCerts/RequestValidator.cs new file mode 100644 index 0000000..e290a12 --- /dev/null +++ b/TameMyCerts/RequestValidator.cs @@ -0,0 +1,907 @@ +// Copyright 2021 Uwe Gradenegger + +// 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. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using CERTENROLLLib; + +namespace TameMyCerts +{ + public class CertificateRequestPolicy + { + public bool AuditOnly { get; set; } + public string KeyAlgorithm { get; set; } = "RSA"; + + public int MinimumKeyLength { get; set; } + public int MaximumKeyLength { get; set; } + public List Subject { get; set; } + public List SubjectAlternativeName { get; set; } + + private static string ConvertToHumanReadableXml(string inputString) + { + var xmlWriterSettings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true, + NewLineOnAttributes = true + }; + + var stringBuilder = new StringBuilder(); + + var xElement = XElement.Parse(inputString); + + using (var xmlWriter = XmlWriter.Create(stringBuilder, xmlWriterSettings)) + { + xElement.Save(xmlWriter); + } + + return stringBuilder.ToString(); + } + + public void SaveToFile(string path) + { + var xmlSerializer = new XmlSerializer(typeof(CertificateRequestPolicy)); + + using (var stringWriter = new StringWriter()) + { + using (var xmlWriter = XmlWriter.Create(stringWriter)) + { + xmlSerializer.Serialize(xmlWriter, this); + var xmlData = stringWriter.ToString(); + + try + { + File.WriteAllText(path, ConvertToHumanReadableXml(xmlData)); + } + catch + { + // fail silently + } + } + } + } + } + + public class SubjectRule + { + public string Field { get; set; } = string.Empty; + public bool Mandatory { get; set; } + public int MaxOccurrences { get; set; } = 1; + public int MinLength { get; set; } = 1; + public int MaxLength { get; set; } = 128; + public List AllowedPatterns { get; set; } + public List DisallowedPatterns { get; set; } + } + + public class CertificateRequestValidator + { + private const string XCN_OID_SUBJECT_ALT_NAME2 = "2.5.29.17"; + private const string XCN_OID_SUBJECT_DIR_ATTRS = "2.5.29.9"; + private const string szOID_RSA_RSA = "1.2.840.113549.1.1.1"; + private const string szOID_ECC_PUBLIC_KEY = "1.2.840.10045.2.1"; + + public CertificateRequestVerificationResult VerifyRequest(string certificateRequest, + CertificateRequestPolicy certificateRequestPolicy, int requestType = CertCli.CR_IN_PKCS10) + { + var result = new CertificateRequestVerificationResult + { + AuditOnly = certificateRequestPolicy.AuditOnly + }; + + #region Extract and parse request + + switch (requestType) + { + case CertCli.CR_IN_CMC: + + // Short form would raise an E_NOINTERFACE exception on Windows 2012 R2 and earlier + var certificateRequestCmc = + (IX509CertificateRequestCmc) Activator.CreateInstance( + Type.GetTypeFromProgID("X509Enrollment.CX509CertificateRequestCmc")); + + // Try to open the Certificate Request + try + { + certificateRequestCmc.InitializeDecode( + certificateRequest, + EncodingType.XCN_CRYPT_STRING_BASE64_ANY + ); + + var oInnerRequest = certificateRequestCmc.GetInnerRequest(InnerRequestLevel.LevelInnermost); + certificateRequest = oInnerRequest.get_RawData(); + } + catch + { + result.Success = false; + result.Description.Add(LocalizedStrings.ReqVal_Err_Extract_From_Cmc); + result.StatusCode = WinError.NTE_FAIL; + return result; + } + finally + { + Marshal.ReleaseComObject(certificateRequestCmc); + GC.Collect(); + } + + break; + + case CertCli.CR_IN_PKCS7: + + // Short form would raise an E_NOINTERFACE exception on Windows 2012 R2 and earlier + var certificateRequestPkcs7 = + (IX509CertificateRequestPkcs7) Activator.CreateInstance( + Type.GetTypeFromProgID("X509Enrollment.CX509CertificateRequestPkcs7")); + + // Try to open the Certificate Request + try + { + certificateRequestPkcs7.InitializeDecode( + certificateRequest, + EncodingType.XCN_CRYPT_STRING_BASE64_ANY + ); + + var oInnerRequest = certificateRequestPkcs7.GetInnerRequest(InnerRequestLevel.LevelInnermost); + certificateRequest = oInnerRequest.get_RawData(); + } + catch + { + result.Success = false; + result.Description.Add(LocalizedStrings.ReqVal_Err_Extract_From_Pkcs7); + result.StatusCode = WinError.NTE_FAIL; + return result; + } + finally + { + Marshal.ReleaseComObject(certificateRequestPkcs7); + GC.Collect(); + } + + break; + } + + // Short form would raise an E_NOINTERFACE exception on Windows 2012 R2 and earlier + var certificateRequestPkcs10 = + (IX509CertificateRequestPkcs10) Activator.CreateInstance( + Type.GetTypeFromProgID("X509Enrollment.CX509CertificateRequestPkcs10")); + + // Try to open the Certificate Request + try + { + certificateRequestPkcs10.InitializeDecode( + certificateRequest, + EncodingType.XCN_CRYPT_STRING_BASE64_ANY + ); + } + catch + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Err_Parse_Request, requestType)); + result.StatusCode = WinError.NTE_FAIL; + return result; + } + + #endregion + + #region Verify key attributes + + // Verify Key Algorithm + string keyAlgorithm; + + switch (certificateRequestPkcs10.PublicKey.Algorithm.Value) + { + case szOID_ECC_PUBLIC_KEY: + keyAlgorithm = "ECC"; + break; + case szOID_RSA_RSA: + keyAlgorithm = "RSA"; + break; + default: + keyAlgorithm = LocalizedStrings.Unknown; + break; + } + + if (certificateRequestPolicy.KeyAlgorithm != keyAlgorithm) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Key_Pair_Mismatch, + keyAlgorithm, certificateRequestPolicy.KeyAlgorithm)); + } + + if (certificateRequestPkcs10.PublicKey.Length < certificateRequestPolicy.MinimumKeyLength) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Key_Too_Small, + certificateRequestPkcs10.PublicKey.Length, certificateRequestPolicy.MinimumKeyLength)); + } + + if (certificateRequestPolicy.MaximumKeyLength > 0) + if (certificateRequestPkcs10.PublicKey.Length > certificateRequestPolicy.MaximumKeyLength) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Key_Too_Large, + certificateRequestPkcs10.PublicKey.Length, certificateRequestPolicy.MaximumKeyLength)); + } + + // Abort here to trigger proper error code + if (result.Success == false) + { + result.StatusCode = WinError.CERTSRV_E_KEY_LENGTH; + return result; + } + + #endregion + + #region Process Subject + + string subjectDn = null; + + try + { + // Will trigger an exception if empty + subjectDn = certificateRequestPkcs10.Subject.Name; + } + catch + { + // Subject is empty + } + + // Convert the Subject DN into a List of Key Value Pairs for each RDN + var subjectRdnList = new List>(); + + if (subjectDn != null) + try + { + subjectRdnList = GetDnComponents(subjectDn); + } + catch + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Err_Parse_SubjectDn, subjectDn)); + result.StatusCode = WinError.CERTSRV_E_BAD_REQUESTSUBJECT; + return result; + } + + #endregion + + #region Process Subject Alternative Name + + // Convert the Subject Alternative Names into a List of Key Value Pairs for each entry + var subjectAltNameList = new List>(); + + // Process Certificate extensions + foreach (IX509Extension extension in certificateRequestPkcs10.X509Extensions) + switch (extension.ObjectId.Value) + { + case XCN_OID_SUBJECT_ALT_NAME2: + + var extensionAlternativeNames = new CX509ExtensionAlternativeNames(); + + extensionAlternativeNames.InitializeDecode( + EncodingType.XCN_CRYPT_STRING_BASE64, + extension.get_RawData(EncodingType.XCN_CRYPT_STRING_BASE64) + ); + + foreach (IAlternativeName san in extensionAlternativeNames.AlternativeNames) + switch (san.Type) + { + case AlternativeNameType.XCN_CERT_ALT_NAME_DNS_NAME: + + subjectAltNameList.Add(new KeyValuePair("dNSName", san.strValue)); + break; + + case AlternativeNameType.XCN_CERT_ALT_NAME_RFC822_NAME: + + subjectAltNameList.Add( + new KeyValuePair("rfc822Name", san.strValue)); + break; + + case AlternativeNameType.XCN_CERT_ALT_NAME_URL: + + subjectAltNameList.Add( + new KeyValuePair("uniformResourceIdentifier", san.strValue)); + break; + + case AlternativeNameType.XCN_CERT_ALT_NAME_USER_PRINCIPLE_NAME: + + subjectAltNameList.Add( + new KeyValuePair("userPrincipalName", san.strValue)); + break; + + case AlternativeNameType.XCN_CERT_ALT_NAME_IP_ADDRESS: + + var b64IpAddress = san.get_RawData(EncodingType.XCN_CRYPT_STRING_BASE64); + var ipAddress = new IPAddress(Convert.FromBase64String(b64IpAddress)); + subjectAltNameList.Add( + new KeyValuePair("iPAddress", ipAddress.ToString())); + + break; + + default: + + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Unsupported_San_Type, + san.ObjectId.Value)); + break; + } + + Marshal.ReleaseComObject(extensionAlternativeNames); + + break; + + // The subject directory attributes extension can be used to convey identification attributes such as the nationality of the certificate subject. + // The extension value is a sequence of OID-value pairs. + case XCN_OID_SUBJECT_DIR_ATTRS: + + // Not supported at the moment + result.Success = false; + result.Description.Add(LocalizedStrings.ReqVal_Unsupported_Extension_Dir_Attrs); + break; + } + + #endregion + + Marshal.ReleaseComObject(certificateRequestPkcs10); + GC.Collect(); + + #region Verify Name constraints + + var subjectValidationResult = VerifySubject( + subjectRdnList, + certificateRequestPolicy.Subject + ); + + if (subjectValidationResult.Success == false) + { + result.Success = false; + result.Description.AddRange(subjectValidationResult.Description); + result.StatusCode = WinError.CERT_E_INVALID_NAME; + } + + var subjectAltNameValidationResult = VerifySubject( + subjectAltNameList, + certificateRequestPolicy.SubjectAlternativeName + ); + + if (subjectAltNameValidationResult.Success == false) + { + result.Success = false; + result.Description.AddRange(subjectAltNameValidationResult.Description); + result.StatusCode = WinError.CERT_E_INVALID_NAME; + } + + #endregion + + return result; + } + + private static CertificateRequestVerificationResult VerifySubject( + List> subjectInfo, List subjectPolicy) + { + var result = new CertificateRequestVerificationResult(); + + if (null == subjectInfo) + { + result.Success = false; + return result; + } + + // Cycle through defined RDNs and compare to present RDNs + foreach (var definedItem in subjectPolicy) + { + // Count the occurrences of the currently inspected defined RDN, if any + var occurrences = subjectInfo.Count(x => x.Key == definedItem.Field); + + // Deny if a RDN defined as mandatory is missing + if (occurrences == 0 && definedItem.Mandatory) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Field_Missing, definedItem.Field)); + } + + // Deny if a RDN occurs too often + if (occurrences > definedItem.MaxOccurrences) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Field_Count_Mismatch, + definedItem.Field, occurrences, definedItem.MaxOccurrences)); + } + } + + foreach (var subjectItem in subjectInfo) + { + var policyItem = subjectPolicy.FirstOrDefault(x => x.Field == subjectItem.Key); + + if (null == policyItem) + { + // Deny if a RDN is found that is not defined (therefore it is forbidden) + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Field_Not_Allowed, subjectItem.Key)); + } + else + { + // Deny if the RDNs content deceeds the defined number of Characters + if (subjectItem.Value.Length < policyItem.MinLength) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Field_Too_Short, subjectItem.Value, + subjectItem.Key, policyItem.MinLength)); + } + + // Deny if the RDNs content exceeds defined number of Characters + if (subjectItem.Value.Length > policyItem.MaxLength) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Field_Too_Long, subjectItem.Value, + subjectItem.Key, policyItem.MaxLength)); + } + + // Process allowed patterns + var allowedMatches = 0; + + if (null == policyItem.AllowedPatterns) + { + result.Success = false; + result.Description.Add( + string.Format(LocalizedStrings.ReqVal_Field_Not_Defined, subjectItem.Key)); + return result; + } + + foreach (var pattern in policyItem.AllowedPatterns) + try + { + if (subjectItem.Key == "iPAddress") + { + var ipAddress = IPAddress.Parse(subjectItem.Value); + + if (ipAddress.IsInRange(pattern)) + allowedMatches++; + } + else + { + var regEx = new Regex(@"" + pattern + ""); + + if (regEx.IsMatch(subjectItem.Value)) + allowedMatches++; + } + } + catch + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Err_Regex, pattern, + subjectItem.Value, subjectItem.Key)); + } + + // Deny if there weren't any matches + if (allowedMatches == 0) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_No_Match, subjectItem.Value, + subjectItem.Key)); + } + + // Process disallowed patterns + if (null != policyItem.DisallowedPatterns) + foreach (var pattern in policyItem.DisallowedPatterns) + try + { + if (policyItem.Field == "iPAddress") + { + var ipAddress = IPAddress.Parse(subjectItem.Value); + if (ipAddress.IsInRange(pattern)) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Disallow_Match, + subjectItem.Value, pattern, subjectItem.Key)); + + // One is sufficient + break; + } + } + else + { + // Stop if the RDN *does* match the defined pattern + var regEx = new Regex(@"" + pattern + ""); + + if (regEx.IsMatch(subjectItem.Value)) + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Disallow_Match, + subjectItem.Value, pattern, subjectItem.Key)); + + // One is sufficient + break; + } + } + } + catch + { + result.Success = false; + result.Description.Add(string.Format(LocalizedStrings.ReqVal_Err_Regex, pattern, + subjectItem.Value, subjectItem.Key)); + + break; + } + } + } + + return result; + } + + public CertificateRequestPolicy LoadFromFile(string path) + { + var xmlSerializer = new XmlSerializer(typeof(CertificateRequestPolicy)); + + try + { + using (var reader = new StreamReader(path)) + { + return (CertificateRequestPolicy) xmlSerializer.Deserialize(reader.BaseStream); + } + } + catch + { + return null; + } + } + + public CertificateRequestPolicy GetSamplePolicy() + { + // This function can be used to write a sample XML based policy configuration file + // This is not in active use by the policy module at the moment + + var policy = new CertificateRequestPolicy + { + KeyAlgorithm = "RSA", + MaximumKeyLength = 4096, + Subject = new List + { + new SubjectRule + { + Field = "commonName", + Mandatory = true, + MaxLength = 64, + AllowedPatterns = new List + { + @"^[-_a-zA-Z0-9]*\.adcslabor\.de$", + @"^[-_a-zA-Z0-9]*\.intra\.adcslabor\.de$" + }, + DisallowedPatterns = new List + { + @"^.*(porn|gambling).*$", + @"^intra\.adcslabor\.de$" + } + }, + new SubjectRule + { + Field = "countryName", + MaxLength = 2, + AllowedPatterns = new List + { + // ISO 3166 country codes as example... to ensure countryName is filled correctly (e.g. "GB" instead of "UK") + @"^(AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)$" + } + }, + new SubjectRule + { + Field = "organizationName", + MaxLength = 64, + AllowedPatterns = new List {@"^ADCS Labor$"} + }, + new SubjectRule + { + Field = "organizationalUnit", + MaxLength = 64, + AllowedPatterns = new List {@"^.*$"} + }, + new SubjectRule + { + Field = "localityName", + AllowedPatterns = new List + { + // All capital cities of german federal states as example + @"^Bremen$", + @"^Hamburg$", + @"^Berlin$", + @"^Saarbruecken$", + @"^Kiel$", + @"^Erfurt$", + @"^Dresden$", + @"^Mainz$", + @"^Magdeburg$", + @"^Wiesbaden$", + @"^Schwerin$", + @"^Potsdam$", + @"^Duesseldorf$", + @"^Stuttgart$", + @"^Hanover$", + @"^Munich$" + } + }, + new SubjectRule + { + Field = "stateOrProvinceName", + AllowedPatterns = new List + { + // All german federal states as example + @"^Bremen$", + @"^Hamburg$", + @"^Berlin$", + @"^Saarland$", + @"^Schleswig Holstein$", + @"^Thuringia$", + @"^Saxony$", + @"^Rhineland Palatinate$", + @"^Saxony-Anhalt$", + @"^Hesse$", + @"^Mecklenburg Western Pomerania$", + @"^Brandenburg$", + @"^Northrhine-Westphalia$", + @"^Baden-Wuerttemberg$", + @"^Lower Saxony$", + @"^Bavaria$" + } + }, + new SubjectRule + { + Field = "emailAddress", + AllowedPatterns = new List {@"^[-_a-zA-Z0-9\.]*\@adcslabor\.de$"} + } + }, + SubjectAlternativeName = new List + { + new SubjectRule + { + Field = "dNSName", + MaxOccurrences = 10, + MaxLength = 64, + AllowedPatterns = new List + { + @"^[-_a-zA-Z0-9]*\.adcslabor\.de$", + @"^[-_a-zA-Z0-9]*\.intra\.adcslabor\.de$" + }, + DisallowedPatterns = new List + { + @"^.*(porn|gambling).*$", + @"^intra\.adcslabor\.de$" + } + }, + new SubjectRule + { + Field = "iPAddress", + MaxOccurrences = 10, + MaxLength = 64, + AllowedPatterns = new List {@"192.168.0.0/16"}, + DisallowedPatterns = new List + { + @"192.168.123.0/24", + @"192.168.127.0/24", + @"192.168.131.0/24" + } + }, + new SubjectRule + { + Field = "userPrincipalName", + MaxLength = 64, + AllowedPatterns = new List {@"^[-_a-zA-Z0-9\.]*\@intra\.adcslabor\.de$"} + }, + new SubjectRule + { + Field = "rfc822Name", + AllowedPatterns = new List {@"^[-_a-zA-Z0-9\.]*\@adcslabor\.de$"} + } + } + }; + + return policy; + } + + public static string SubstituteRdnTypeAliases(string rdnType) + { + // Convert all known aliases used by the Microsoft API to the "official" name as specified in ITU-T X.520 and/or RFC 4519 + // https://www.itu.int/itu-t/recommendations/rec.aspx?rec=X.520 + // https://datatracker.ietf.org/doc/html/rfc4519#section-2 + + // Here are some sources the below list is based on + // https://www.gradenegger.eu/?p=2717 + // https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certstrtonamea + // https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-osco/dbdc3411-ed0a-4713-a01b-1ae0da5e75d4 + + switch (rdnType.ToUpperInvariant()) + { + // The default ones active on an ADCS CA + case "C": return "countryName"; + case "CN": return "commonName"; + case "DC": return "domainComponent"; + case "E": return "emailAddress"; + case "L": return "localityName"; + case "O": return "organizationName"; + case "OU": return "organizationalUnit"; + case "S": return "stateOrProvinceName"; + + // These can get enabled in addition to the default ones (in the SubjectTemplate registry key) + case "G": return "givenName"; + case "I": return "initials"; + case "SN": return "surname"; + case "STREET": return "streetAddress"; + case "T": return "title"; + + // These automatically get enabled if an NDES server gets deployed for the CA + case "UNSTRUCTUREDNAME": return "unstructuredName"; + case "UNSTRUCTUREDADDRESS": return "unstructuredAddress"; + case "DEVICESERIALNUMBER": return "deviceSerialNumber"; + + // These are only useable if the CRLF_REBUILD_MODIFIED_SUBJECT_ONLY flag is enabled on the CA, which allows + // any subject DN to get specified by the enrollee. But this is fine when properly using this policy module + case "POSTALCODE": return "postalCode"; + case "DESCRIPTION": return "description"; + case "POBOX": return "postOfficeBox"; + case "PHONE": return "telephoneNumber"; + + // Unknown ones get returned as they are + default: return rdnType; + } + } + + // If the subject RDN contains quotes or special characters, the IX509CertificateRequest interface escapes these with quotes + // As this messes up our comparison logic, we must remove the additional quotes + private static string RemoveQuotesFromSubjectRdn(string rdn) + { + if (null == rdn) + return null; + + if (rdn.Length == 0) + return rdn; + + // Not in quotes, nothing to do + if (rdn[0] != '"' && rdn[rdn.Length - 1] != '"') + return rdn; + + // Skip first and last char, then remove every 2nd quote + + const char quoteChar = '\"'; + var inQuotedString = false; + var outString = string.Empty; + + for (var i = 1; i < rdn.Length - 1; i++) + { + var currentChar = rdn[i]; + + if (currentChar == quoteChar) + { + if (inQuotedString == false) + outString += currentChar; + + inQuotedString = !inQuotedString; + } + else + { + outString += currentChar; + } + } + + return outString; + } + + public static List> GetDnComponents(string distinguishedName) + { + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + // https://github.com/dotnet/corefx/blob/c539d6c627b169d45f0b4cf1826b560cd0862abe/src/System.DirectoryServices/src/System/DirectoryServices/ActiveDirectory/Utils.cs#L440-L449 + + // First split by ',' + var components = Split(distinguishedName, ','); + + if (null == components) + return null; + + var dnComponents = new List>(); + + for (var i = 0; i < components.GetLength(0); i++) + { + // split each component by '=' + var subComponents = Split(components[i], '='); + + if (subComponents.GetLength(0) != 2) throw new ArgumentException(); + + var key = SubstituteRdnTypeAliases(subComponents[0].Trim()); + var value = RemoveQuotesFromSubjectRdn(subComponents[1].Trim()); + + if (key.Length > 0) + dnComponents.Add(new KeyValuePair(key, value)); + else + throw new ArgumentException(); + } + + return dnComponents; + } + + public static string[] Split(string distinguishedName, char delimiter) + { + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + // https://github.com/dotnet/corefx/blob/c539d6c627b169d45f0b4cf1826b560cd0862abe/src/System.DirectoryServices/src/System/DirectoryServices/ActiveDirectory/Utils.cs#L440-L449 + + if (null == distinguishedName) + return null; + + if (distinguishedName.Length == 0) + return null; + + var inQuotedString = false; + const char quoteChar = '\"'; + const char escapeChar = '\\'; + var nextTokenStart = 0; + var resultList = new ArrayList(); + + // get the actual tokens + for (var i = 0; i < distinguishedName.Length; i++) + { + var currentChar = distinguishedName[i]; + + if (currentChar == quoteChar) + { + inQuotedString = !inQuotedString; + } + else if (currentChar == escapeChar) + { + // skip the next character (if one exists) + if (i < distinguishedName.Length - 1) i++; + } + else if (!inQuotedString && currentChar == delimiter) + { + // we found an unquoted character that matches the delimiter + // split it at the delimiter (add the token that ends at this delimiter) + resultList.Add(distinguishedName.Substring(nextTokenStart, i - nextTokenStart)); + nextTokenStart = i + 1; + } + + if (i == distinguishedName.Length - 1) + { + // we've reached the end + + // if we are still in quoted string, the format is invalid + if (inQuotedString) throw new ArgumentException(); + + // we need to end the last token + resultList.Add(distinguishedName.Substring(nextTokenStart, i - nextTokenStart + 1)); + } + } + + var results = new string[resultList.Count]; + for (var i = 0; i < resultList.Count; i++) results[i] = (string) resultList[i]; + return results; + } + + public class CertificateRequestVerificationResult + { + public int StatusCode = WinError.ERROR_SUCCESS; + public bool Success { get; set; } = true; + + public bool AuditOnly { get; set; } + public List Description { get; set; } = new List(); + } + } +} \ No newline at end of file diff --git a/TameMyCerts/SamplePolicy.xml b/TameMyCerts/SamplePolicy.xml new file mode 100644 index 0000000..d70fe56 --- /dev/null +++ b/TameMyCerts/SamplePolicy.xml @@ -0,0 +1,153 @@ + + RSA + 2048 + 4096 + + + commonName + true + 1 + 64 + + ^[-_a-zA-Z0-9]*\.adcslabor\.de$ + ^[-_a-zA-Z0-9]*\.intra\.adcslabor\.de$ + + + ^.*(porn|gambling).*$ + ^intra\.adcslabor\.de$ + + + + countryName + false + 1 + 2 + + ^(AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)$ + + + + organizationName + false + 1 + 64 + + ^ADCS Labor$ + + + + organizationalUnit + false + 1 + 64 + + ^.*$ + + + + localityName + false + 1 + 128 + + ^Bremen$ + ^Hamburg$ + ^Berlin$ + ^Saarbruecken$ + ^Kiel$ + ^Erfurt$ + ^Dresden$ + ^Mainz$ + ^Magdeburg$ + ^Wiesbaden$ + ^Schwerin$ + ^Potsdam$ + ^Duesseldorf$ + ^Stuttgart$ + ^Hanover$ + ^Munich$ + + + + stateOrProvinceName + false + 1 + 128 + + ^Bremen$ + ^Hamburg$ + ^Berlin$ + ^Saarland$ + ^Schleswig Holstein$ + ^Thuringia$ + ^Saxony$ + ^Rhineland Palatinate$ + ^Saxony-Anhalt$ + ^Hesse$ + ^Mecklenburg Western Pomerania$ + ^Brandenburg$ + ^Northrhine-Westphalia$ + ^Baden-Wuerttemberg$ + ^Lower Saxony$ + ^Bavaria$ + + + + emailAddress + false + 1 + 128 + + ^[-_a-zA-Z0-9\.]*\@adcslabor\.de$ + + + + + + dNSName + false + 10 + 64 + + ^[-_a-zA-Z0-9]*\.adcslabor\.de$ + ^[-_a-zA-Z0-9]*\.intra\.adcslabor\.de$ + + + ^.*(porn|gambling).*$ + ^intra\.adcslabor\.de$ + + + + iPAddress + false + 10 + 64 + + 192.168.0.0/16 + + + 192.168.123.0/24 + 192.168.127.0/24 + 192.168.131.0/24 + + + + userPrincipalName + false + 1 + 64 + + ^[-_a-zA-Z0-9\.]*\@intra\.adcslabor\.de$ + + + + rfc822Name + false + 1 + 128 + + ^[-_a-zA-Z0-9\.]*\@adcslabor\.de$ + + + + \ No newline at end of file diff --git a/TameMyCerts/TameMyCerts.csproj b/TameMyCerts/TameMyCerts.csproj new file mode 100644 index 0000000..cd4d185 --- /dev/null +++ b/TameMyCerts/TameMyCerts.csproj @@ -0,0 +1,99 @@ + + + + + Debug + AnyCPU + {BB35A67E-8E22-48C3-B3F8-E852161ACB59} + Library + Properties + TameMyCerts + TameMyCerts + v4.6 + 512 + true + + + + true + full + true + bin\Debug\ + TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + .\CERTCLILib.dll + True + + + .\CERTPOLICYLib.dll + True + + + + + + + + + + + + + True + True + AutoVersionIncrement.tt + + + + True + True + LocalizedStrings.resx + + + + + + + + + + + + {728AB348-217D-11DA-B2A4-000E7BBB2B09} + 1 + 0 + 0 + tlbimp + False + True + + + + + TextTemplatingFileGenerator + AutoVersionIncrement.cs + + + + + + + + ResXFileCodeGenerator + LocalizedStrings.Designer.cs + + + + \ No newline at end of file diff --git a/TameMyCerts/TemplateInfo.cs b/TameMyCerts/TemplateInfo.cs new file mode 100644 index 0000000..fbd9c1f --- /dev/null +++ b/TameMyCerts/TemplateInfo.cs @@ -0,0 +1,88 @@ +// Copyright 2021 Uwe Gradenegger + +// 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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Win32; + +namespace TameMyCerts +{ + public class TemplateInfo + { + private readonly int _refreshInterval; + private DateTime _lastRefreshTime = new DateTime(1970, 1, 1); + private List