1

Lots of similar questions, but I don't see a good answer.

I have a cloud-based application with an agent installed on customer sites in the field. I want to have the agent, upon installation, generate a CSR and upload the request over HTTPS to the cloud service, where I will sign it with the cloud service certificate (that the agent will trust) and return the signed certificate. This certificate will be used for agent authentication, encryption, and file signatures.

I'm able to make the CSR pretty easily:

    public string GenerateAgentCsr(Guid customerId)
    {
        var domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName;
        var domainDistinguishedName = string.Join(",DC=", domain.Split("."));
        var distinguishedName = $"DC={Environment.MachineName},DC={domainDistinguishedName},O={customerId}";
        var rsa = RSA.Create(1024);
        var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
        request.CertificateExtensions.Add(new X509KeyUsageExtension(
            X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.DataEncipherment |
            X509KeyUsageFlags.NonRepudiation, false));

        var armored = request.AsCsr();
        return armored;
    }

    public static string AsCsr(this CertificateRequest request)
    {
        var encoded = request.CreateSigningRequest();
        var payload = Convert.ToBase64String(encoded);
        using var stream = new MemoryStream();
        using (var writer = new StreamWriter(stream, Encoding.UTF8, 512, true))
        {
            writer.WriteLine("-----BEGIN CERTIFICATE REQUEST-----");
            writer.WriteLine(payload.Slice());
            writer.WriteLine("-----END CERTIFICATE REQUEST-----");
            writer.Flush();
        }

        stream.Position = 0;
        using (var reader = new StreamReader(stream))
        {
            return reader.ReadToEnd();
        }
    }

...so I can send this up to the cloud server, but then I immediately run into a problem. My plan was to have a method like so:

    public X509Certificate2 SignCertificate(CertificateRequest csr)
    {
        var signedCert = csr.Create(GetCertificateAuthorityCertificate(), DateTimeOffset.Now, DateTimeOffset.Now.AddYears(10),
            new byte[] { 1, 2, 3, 4 });
        return signedCert;
    }

...but I can't actually convert the CSR into a CertificateRequest. There's no Parse mechanism or constructor that will allow this.

So... once I have the CSR, how do I properly parse and sign it with my cloud service certificate so that I can send it back to the agent?

Jeremy Holovacs
  • 22,480
  • 33
  • 117
  • 254

1 Answers1

3

The only way that types will help you with populating a CertificateRequest object is by reading the data with System.Formats.Asn1.AsnReader.

Signing a certificate request is a major decision. Does the request use only names that it is authorized to use? Is it promoting itself to another issuing certificate authority? Are there any extensions requested that you don't understand the meaning of? Is it claiming to be a TimeStamp Authority, or a Code Signing Certificate, or any other limited-purpose?

By signing an external request you're trying to be a "real" CA. Are you doing revocation? Are you logging the issuance?

.NET has the SubjectAlternativeNameBuilder to help build SAN extensions, but doesn't have a good way to read them back... because SubjectAlternativeNames are much more complex than they generally are used as. So there's no good way to answer the names question.

A much more limited version of this, if you're using controlled access, would be to transmit just the SubjectPublicKeyInfo value of the public key (and whatever other information you want to include) in your own custom format (see also rsa.ExportSubjectPublicKeyInfo()). Then you know you're not being asked to do something that you don't want to do. Unfortunately, PublicKey.CreateFromSubjectPublicKeyInfo is new in .NET 6. Fortunately, it's easy:

private static PublicKey CreateFromSubjectPublicKeyInfo(ReadOnlyMemory<byte> data)
{
    AsnReader reader = new AsnReader(data, AsnEncodingRules.DER);
    AsnReader spki = reader.ReadSequence();
    reader.ThrowIfNotEmpty();

    AsnReader algorithmIdentifier = spki.ReadSequence();
    string algOid = algorithmIdentifier.ReadObjectIdentifier();
    ReadOnlyMemory<byte> algParameters = default;

    if (algOid.HasData)
    {
        algParameters = algorithmIdentifier.ReadEncodedValue();
    }

    algorithmIdentifier.ThrowIfNotEmpty();

    byte[] publicKey = spki.ReadBitString(out _);
    spki.ThrowIfNotEmpty();

    return new PublicKey(
        new Oid(algOid, null),
        algParameters.ToArray(),
        publicKey);
}
bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • The idea would be to manage revocation, yes... I may end up just doing self-signed with public keys though, that may be sufficient for what I'm trying to accomplish. – Jeremy Holovacs Jan 28 '21 at 20:19