Closed jeremyVignelles closed 1 year ago
There are N things that I think are important before API is added here:
1) The ability to create (and probably read) CRLs 2) The ability to encode CRL Distribution Point values into cert extensions 3) The ability to encode the Authority Information Access certificate extension (for OCSP). 4) Maybe the ability to read OCSP requests and write OCSP responses. 5) Figuring out what to do with the PKCS#10 extensions (like challenge request). 6) Being able to reason over the SAN entries 7) Certificate Transparency might be good, too.
That still leaves out the fundamentally hard problems like "knowing if it's reasonable to sign the SAN entries", but that's a CA policy thing. The important part, to me, from the platform perspective is to make it such that anyone writing any form of CA be capable of executing on the main parts of policy. Effectively, if one were to walk through the CA Browser Forum's Baseline Requirements document, would they be capable of writing a conforming CA.... if not, I'd rather not have the ability to read the CSR.
If the only needs from your CA solution are "I want your public key, and I'm going to make up the rest from some external context data", that's best conveyed by the client sending their key with the new (in Core 3.0) AsymmetricAlgorithm.ExportSubjectPublicKeyInfo()
method (family), then the CA side hydrating it into an appropriate key (it should probably be hydratable directly to a PublicKey object, but I missed that)... then use the CertficateRequest to line up the rest of the extensions (which have no surprises, since you added them instead of decoded them) and sign it.
So, effectively, this request is the very end of a long chain of feature requests for a "I want to make a Certificate Authority with .NET". The goal of the API was just to be able to submit requests to a good CA, or to build certificates for testing scenarios... with the other half of the equation a giant TBD :smile:.
Thanks for your answer, that makes sense. I guess I'll have to do it another way I guess. The PKCS#10 was a way for me to have the client apps generate alternative names based on local IPs and send them to the CA. The client is already authenticated by other means, so I don't really need a fully compliant CA.
a way for me to have the client apps generate alternative names based on local IPs and send them to the CA
You could, if it fits into your security model, have the client send a list of IP addresses, or take the encoded output of the SubjectAlternativeNameBuilder, and send that in your protocol (perhaps alongside the SubjectPublicKeyInfo).
Yeah, it asymptotically approaches "just build/send and receive/read a PKCS#10", but doing things piecemeal, like not allowing the client to assert that it is, itself, a subordinate CA.
+1 to deserialize a CertificateRequest byte array back into a CertificateRequest object for delayed signing.
Agree this is conceptually at the long tail end of implementing a CA which is CA/B compliant and might look a bit like a public authority, but short of this there are there not myriad private and internal use-cases where signing a received CSR is a useful mechanic?
Suppose I serialize it and write it to disk. Now I want to read it back in?
I certainly don't want to have to dive into openssl or, worse, BouncyCastle. Or the other way around, depending on preference.
We are currently developing a solution that needs this feature. I agree that numerous things must be added before a full-fledged CA can be developed using .NET. But when I'm able to manually create and serialize a certificate request, why shouldn't I be able to deserialize a certificate request? Now I am probably going to have to interop with OpenSSL :(.
@bartonjs
Sounds like there is some interest in this, so I would propose something like this, inspired from what we have an X509Certificate2
.
namespace System.Security.Cryptography.X509Certificates {
public partial class CertificateRequest {
+ public CertificateRequest(byte[] derContents);
+ public CertificateRequest(ReadOnlySpan<byte> derContents);
+ public static CertificateRequest CreateFromPem(ReadOnlySpan<char> csrPem);
}
}
The API proposal isn't the hard part :smile:. I'm still opposed to adding this functionality until at least numbers 1-3 in https://github.com/dotnet/runtime/issues/29547#issuecomment-492420114 are tackled. But, getting to the point where this can be added to .NET 7 (then adding it) is my top personal goal for .NET 7.
Common File Extensions:
Extension | Detail | Status |
---|---|---|
.pem | X509Certificate2 cert = new X509Certificate2.CreateFromPem(@"C:\test.pem", @"C:\key.pem"); | |
.crt | X509Certificate2 cert = new X509Certificate2(@"C:\test.crt"); | |
.p7b | X509Certificate2Collection certCollection = new X509Certificate2Collection(); certCollection.Import(@"C:\test.p7b"); |
|
.der | https://stackoverflow.com/questions/60760681/read-der-file-private-key-c-sharp | ? |
.cer | X509Certificate2 cert = new X509Certificate2(@"C:\test.cer"); | |
.pfx | X509Certificate2Collection certCollection = new X509Certificate2Collection(); certCollection.Import(@"C:\test.pfx", "password", X509KeyStorageFlags.PersistKeySet); |
|
.p12 | X509Certificate2Collection certCollection = new X509Certificate2Collection(); certCollection.Import(@"C:\test.p12", "password", X509KeyStorageFlags.PersistKeySet); |
|
.crl | Certificate Revocation List | ? |
.csr | Certificate Signing Request | ? |
.dll and .so | PKCS#11 dll (windows) and so (linux) file for load certificates from token or hsm | ? |
@vcsjones @bartonjs
@vcsjones I still need to work in X509RevocationReason. Did you envision it as a nullable parameter to AddEntry (e.g. AddEntry(ReadOnlySpan<byte> serialNumber, DateTimeOffset revocationTime, X509RevocationReason? reason = default)
), or just double the overloads tacking it on the end as non-nullable... or overloads with non-defaulted nullable? (I slightly worry an overload with non-nullable might suggest we're providing a value in the other overloads, but that it's too complicated to represent with a default)
@bartonjs I envisioned something like this, both revocationTime
and reason
as nullable, since I think your original draft had overloads with and without revocationTime
, I just switched everything to nullable to avoid an overloadsplosion.
// Absolutely not tied to this name.
public enum CertificateRevocationListRevokeReason {
Unspecified = 0,
KeyCompromise = 1,
CACompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
// No 7, per RFC 5280,
RemoveFromCRL = 8,
PrivilegeWithdrawn = 9,
AACompromise = 10,
}
// CRLBuilder AddEntry:
public void AddEntry(
byte[] serialNumber,
DateTimeOffset? revocationTime = null,
CertificateRevocationListRevokeReason? reason = null);
public void AddEntry(
ReadOnlySpan<byte> serialNumber,
DateTimeOffset? revocationTime = null,
CertificateRevocationListRevokeReason? reason = null);
public void AddEntry(
X509Certificate2 certificate,
DateTimeOffset? revocationTime = null,
CertificateRevocationListRevokeReason? reason = null);
null
reason does not mean CertificateRevocationListRevokeReason.Unspecified
, it means the extension is omitted entirely. Hence the distinction between making it a nullable enum vs. the default just being "Unspecified".
CertificateRevocationListBuilder
Build
: let's make thisUpdate
optionalExpireEntries
until someone asks for it CertificateRequest
let's not use unsafe
parameters because they can disappear from the call sites. Suggestion from YouTube was to use an enum, which we liked.namespace System.Security.Cryptography.X509Certificates;
public sealed partial class CertificateRevocationListBuilder
{
public CertificateRevocationListBuilder();
public void AddEntry(byte[] serialNumber,
DateTimeOffset? revocationTime = default,
X509RevocationReason? reason = default);
public void AddEntry(ReadOnlySpan<byte> serialNumber,
DateTimeOffset? revocationTime = default,
X509RevocationReason? reason = default);
public void AddEntry(X509Certificate2 certificate,
DateTimeOffset? revocationTime = default,
X509RevocationReason? reason = default);
public byte[] Build(X509Certificate2 issuerCertificate,
BigInteger crlNumber,
DateTimeOffset nextUpdate,
HashAlgorithmName hashAlgorithm,
RSASignaturePadding? rsaSignaturePadding = null,
DateTimeOffset? thisUpdate = null);
public byte[] Build(X500DistinguishedName issuerName,
X509SignatureGenerator generator,
BigInteger crlNumber,
DateTimeOffset nextUpdate,
HashAlgorithmName hashAlgorithm,
X509AuthorityKeyIdentifierExtension akid,
DateTimeOffset? thisUpdate = null));
public static CertificateRevocationListBuilder Load(byte[] currentCrl,
out BigInteger currentCrlNumber);
public static CertificateRevocationListBuilder Load(ReadOnlySpan<byte> currentCrl,
out BigInteger currentCrlNumber,
out int bytesConsumed);
public static CertificateRevocationListBuilder LoadPem(string currentCrl,
out BigInteger currentCrlNumber);
public static CertificateRevocationListBuilder LoadPem(ReadOnlySpan<char> currentCrl,
out BigInteger currentCrlNumber);
public bool RemoveEntry(byte[] serialNumber);
public bool RemoveEntry(ReadOnlySpan<byte> serialNumber);
public static X509Extension BuildCrlDistributionPointExtension(IEnumerable<string> uris,
bool critical = false);
}
public enum X509RevocationReason
{
Unspecified = 0,
KeyCompromise = 1,
CACompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
RemoveFromCrl = 8,
PrivilegeWithdrawn = 9,
AACompromise = 10,
WeakAlgorithmOrKey = 11,
}
public partial class CertificateRequest
{
public CertificateRequest(X500DistinguishedName subjectName,
PublicKey publicKey,
HashAlgorithmName hashAlgorithm,
RSASignaturePadding? rsaSignaturePadding = default);
public Collection<AsnEncodedData> OtherRequestAttributes { get; }
public static CertificateRequest LoadSigningRequest(byte[] pkcs10,
HashAlgorithmName signerHashAlgorithm,
CertificateRequestLoadOptions options = default,
RSASignaturePadding? signerSignaturePadding = null);
public static CertificateRequest LoadSigningRequest(ReadOnlySpan<byte> pkcs10,
HashAlgorithmName signerHashAlgorithm,
out int bytesConsumed,
CertificateRequestLoadOptions options = default,
RSASignaturePadding? signerSignaturePadding = null);
public static CertificateRequest LoadSigningRequestPem(ReadOnlySpan<char> pkcs10Pem,
HashAlgorithmName signerHashAlgorithm,
CertificateRequestLoadOptions options = default,
RSASignaturePadding? signerSignaturePadding = null);
public static CertificateRequest LoadSigningRequestPem(string pkcs10Pem,
HashAlgorithmName signerHashAlgorithm,
CertificateRequestLoadOptions options = default,
RSASignaturePadding? signerSignaturePadding = null);
public string CreateSigningRequestPem();
public string CreateSigningRequestPem(X509SignatureGenerator signatureGenerator);
}
[Flags]
public enum CertificateRequestLoadOptions
{
Default = 0x0,
SkipSignatureValidation = 0x01,
UnsafeLoadCertificateExtensions = 0x02
}
public partial class X509BasicConstraintsExtension
{
public static X509BasicConstraintsExtension CreateForCertificateAuthority(int? pathLengthConstraint = null);
public static X509BasicConstraintsExtension CreateForEndEntity(bool critical = false);
}
Rationale
I am using the
CertificateRequest.CreateSigningRequest()
to get a CSR byte array that I can send to the Certificate Authority.As far as I know, there is currently no way, on the CA side, to "deserialize" this byte array into a
CertificateRequest
.Use case
I am currently building a CA in .NET, that can sign certificate for .net client applications.
The .net client generates a PKCS 10 CSR, and send it over HTTPS to the CA, which will sign it.
Proposed API
Policies and best practices dictate that we want callers to be able to provide basic certificate revocation via CRL (Certificate Revocation Lists) prior to enabling them to load a PKCS#10 signing request, so CRL building is included in this proposal.
A CRL is, essentially, a signed list of
(byte[] SerialNumber, DateTimeOffset RevocationTime, X509Extension[] Extensions)
.Build a CRL from nothing
Load an existing CRL, modify it, save it back
Creating a CRL Distribution Points Extension for Generated Certificates
At this time we don't think it's important to expose a way to read these back, and we don't support generating them in their full complexity, so we provide a helper-builder on the closest appropriate type (without burning the best type name for if we add a rich type later).
Support PKCS#10 attributes, load a PKCS#10
Usability/Understanding Improvement in X509BasicConstraintsExtension