Creating a C# SCEP server
Asked Answered
A

0

6

I am trying to build a SCEP server to support Apple MDM Device Enrollment. This needs to be implemented into our current MDM Service, written in C#.

I have looked into the following for inspiration:

  1. JSCEP, a java library for scep server implementation https://github.com/jscep/jscep
  2. Bouncy Castle (complicated, and not a lot of documentation on the C# side)
  3. Cisco SCEP documentation http://www.cisco.com/c/en/us/support/docs/security-vpn/public-key-infrastructure-pki/116167-technote-scep-00.html

Does anyone know of any solid examples, on creating a C# SCEP server? I haven't been able to find any good documentation for this.

UPDATE

This is what i have by now, it is still not working, but i think the issue lies with the signed pkcs7, but I am not sure what i am missing?

    public class ScepModule: NancyModule
{
    /// <summary>
    /// The _log.
    /// </summary>
    private readonly ILog log = LogManager.GetLogger(typeof(ScepModule));
    /// <summary>
    /// Initializes a new instance of the <see cref="ScepModule"/> class.
    /// </summary>
    /// <param name="cp">
    /// The certificate provider.
    /// </param>
    /// <param name="config">
    /// The config.
    /// </param>
    public ScepModule(MdmConfigDTO config)
        : base("/cimdm/scep")
    {
        this.log.Debug(m => m("Instanciating scep Module."));
        this.Get["/"] = result =>
        {
            var message = Request.Query["message"];
            var operation = Request.Query["operation"];
            if (operation == "GetCACert")
            {
                return RespondWithCACert();
            }
            else if (operation == "GetCACaps")
            {
                return RespondWithCACaps();
            }
            return "";
        };
        this.Post["/"] = result =>
        {
            var message = Request.Query["message"];
            var operation = Request.Query["operation"];

            byte[] requestData = null;

            using (var binaryReader = new BinaryReader(Request.Body))
            {
                requestData = binaryReader.ReadBytes((int)Request.Body.Length);
            }

            var headers = Request.Headers;
            foreach (var header in headers)
            {
                this.log.Debug(m => m("Header: {0}, Value: {1}", header.Key, String.Join(",", header.Value)));
            }

            var caCert = getSignerCert();

            var signingCert = createSigningCert(caCert, requestData, config);

            return signingCert;
        };
        this.log.Debug(m => m("Finished Instanciating scep Module."));
    }


    private Response RespondWithCACert()
    {
        var caCert = getSignerCert();

        var response = new Response();
        response.ContentType = "application/x-x509-ca-cert";
        response.Contents = stream =>
        {
            byte[] data = caCert.Export(X509ContentType.Cert);
            stream.Write(data, 0, data.Length);
            stream.Flush();
            stream.Close();
        };
        return response;
    }

    private Response RespondWithCACaps()
    {
        var response = new Response();
        response.ContentType = "text/plain; charset=ISO-8859-1";
        //byte[] data = Encoding.UTF8.GetBytes("POSTPKIOperation\nSHA-512\nSHA-256\nSHA-1");
        byte[] data = Encoding.UTF8.GetBytes("POSTPKIOperation\nSHA-1");

        response.Contents = stream =>
        {
            stream.Write(data, 0, data.Length);
            stream.Flush();
            stream.Close();
        };
        return response;
    }

    private Response createSigningCert(X509Certificate2 caCert, byte[] data, MdmConfigDTO config)
    {
        var signedResponse = new SignedCms();
        signedResponse.Decode(data);

        var caChain = new X509Certificate2Collection(caCert);

        signedResponse.CheckSignature(caChain, true);

        var attributes = signedResponse
            .SignerInfos
            .Cast<System.Security.Cryptography.Pkcs.SignerInfo>()
            .SelectMany(si => si.SignedAttributes.Cast<CryptographicAttributeObject>());

        // Any errors then return null
        if (attributes.Any(att => att.Oid.Value.Equals(Oids.Scep.FailInfo)))
        {
            return null;
        }

        byte[] msg = DecryptMsg(signedResponse.ContentInfo.Content, caChain);
        byte[] certResult = GenerateSelfSignedClientCertificate(msg, caCert, config);
        X509Certificate2Collection reqCerts = signedResponse.Certificates;

        //Create Enveloped PKCS#7 data
        var envelpeDataPkcs7 = createEnvelopedDataPkcs7(certResult);

        //Create Signed PKCS#7 data
        var signedDataPkcs7 = createSignedDataPkcs7(envelpeDataPkcs7, caCert, attributes);


        var response = new Response();
        response.ContentType = "application/x-pki-message";
        response.WithHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.WithHeader("Pragma", "no-cache");

        var execPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        File.WriteAllBytes(execPath + "\\signedDataPkcs7.p7b", signedDataPkcs7);
        File.WriteAllBytes(execPath + "\\envelpeDataPkcs7.p7b", envelpeDataPkcs7);

        response.Contents = stream =>
        {
            stream.Write(signedDataPkcs7, 0, signedDataPkcs7.Length);
            stream.Flush();
            stream.Close();
        };
        return response;
    }

    private byte[] GenerateSelfSignedClientCertificate(byte[] encodedPkcs10, X509Certificate2 caCert, MdmConfigDTO config)
    {
        var execPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        File.WriteAllText(execPath + "\\scepPkcs10.csr", "-----BEGIN CERTIFICATE REQUEST-----\n" + Convert.ToBase64String(encodedPkcs10) + "\n-----END CERTIFICATE REQUEST-----");

        Pkcs10CertificationRequest csr = new Pkcs10CertificationRequest(encodedPkcs10);

        CertificationRequestInfo csrInfo = csr.GetCertificationRequestInfo();
        SubjectPublicKeyInfo pki = csrInfo.SubjectPublicKeyInfo;

        Asn1Set attributes = csrInfo.Attributes;
        //pub key for the signed cert
        var publicKey = pki.GetPublicKey();

        // Build a Version3 Certificate
        DateTime startDate = DateTime.UtcNow.AddMonths(-1);
        DateTime expiryDate = startDate.AddYears(10);
        BigInteger serialNumber = new BigInteger(32, new Random());

        this.log.Debug(m => m("Certificate Signing Request Subject is: {0}", csrInfo.Subject));
        CmsSignedDataGenerator cmsGen = new CmsSignedDataGenerator();

        X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
        string signerCN = caCert.GetNameInfo(X509NameType.SimpleName, true);

        certGen.SetSubjectDN(new X509Name(String.Format("CN={0}", config.HostName)));

        certGen.SetSerialNumber(serialNumber);
        certGen.SetIssuerDN(new X509Name(String.Format("CN={0}", signerCN)));
        certGen.SetNotBefore(startDate);
        certGen.SetNotAfter(expiryDate);
        certGen.SetSignatureAlgorithm("SHA1withRSA");
        certGen.SetPublicKey(PublicKeyFactory.CreateKey(pki));

        AsymmetricCipherKeyPair caPair = DotNetUtilities.GetKeyPair(caCert.PrivateKey);

        for(int i=0; i!=attributes.Count; i++)
        {
            AttributeX509 attr = AttributeX509.GetInstance(attributes[i]);

            //process extension request
            if (attr.AttrType.Id.Equals(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest.Id))
            {
                X509Extensions extensions = X509Extensions.GetInstance(attr.AttrValues[0]);

                var e = extensions.ExtensionOids.GetEnumerator();
                while (e.MoveNext())
                {
                    DerObjectIdentifier oid = (DerObjectIdentifier)e.Current;
                    Org.BouncyCastle.Asn1.X509.X509Extension ext = extensions.GetExtension(oid);

                    certGen.AddExtension(oid, ext.IsCritical, ext.GetParsedValue());
                }
            }
        }

        //certGen.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment));
        certGen.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeID.AnyExtendedKeyUsage));
        certGen.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(0));

        Org.BouncyCastle.X509.X509Certificate selfSignedCert = certGen.Generate(caPair.Private);

        //Check if the certificate can be verified
        selfSignedCert.Verify(caPair.Public);

        if (log.IsDebugEnabled)
        {
            //var execPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
            File.WriteAllBytes(execPath + "\\scepCertificate.pfx", selfSignedCert.GetEncoded());
        }

         return selfSignedCert.GetEncoded();
    }

    private byte[] createEnvelopedDataPkcs7(byte[] pkcs7RequestData)
    {
        var recipient = new CmsRecipient(new X509Certificate2(pkcs7RequestData));

        var envelopedContent = new System.Security.Cryptography.Pkcs.ContentInfo(new Oid(Oids.Pkcs7.EncryptedData, "envelopedData"), pkcs7RequestData);
        //var envelopedContent = new System.Security.Cryptography.Pkcs.ContentInfo(pkcs7RequestData);
        var envelopedMessage = new EnvelopedCms(envelopedContent);
        //var envelopedMessage = new EnvelopedCms(Con);

        envelopedMessage.Encrypt(recipient);
        var encryptedMessageData = envelopedMessage.Encode();

        return encryptedMessageData;
    }

    private byte[] createSignedDataPkcs7(byte[] encryptedMessageData, X509Certificate2 localPrivateKey, IEnumerable<CryptographicAttributeObject> attributes)
    {

        var senderNonce = attributes
            .Single(att => att.Oid.Value.Equals(Oids.Scep.SenderNonce))
            .Values[0];

        var transactionId = attributes
            .Single(att => att.Oid.Value.Equals(Oids.Scep.TransactionId))
            .Values[0];

        // Create the outer envelope, signed with the local private key
        var signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, localPrivateKey);
        //{
        //    DigestAlgorithm = new Oid(Oids.Pkcs.SHA1, "digestAlorithm")
        //};

        //signer.SignedAttributes.Add(signingTime);
        var messageType = new AsnEncodedData(Oids.Scep.MessageType, DerEncoding.EncodePrintableString("3"));
        signer.SignedAttributes.Add(messageType);
        signer.SignedAttributes.Add(transactionId);
        var recipientNonce = new Pkcs9AttributeObject(Oids.Scep.RecipientNonce, DerEncoding.EncodeOctet(senderNonce.RawData));
        signer.SignedAttributes.Add(recipientNonce);

        var pkiStatus = new AsnEncodedData(Oids.Scep.PkiStatus, DerEncoding.EncodePrintableString("0"));
        signer.SignedAttributes.Add(pkiStatus);

        var nonceBytes = new byte[16];
        RNGCryptoServiceProvider.Create().GetBytes(nonceBytes);
        senderNonce = new Pkcs9AttributeObject(Oids.Scep.SenderNonce, DerEncoding.EncodeOctet(nonceBytes));
        signer.SignedAttributes.Add(senderNonce);


        //var failInfo = new Pkcs9AttributeObject(Oids.Scep.FailInfo, DerEncoding.EncodePrintableString("2"));
        //signer.SignedAttributes.Add(failInfo);

        // Seems that the oid is not needed for this envelope
        var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(encryptedMessageData); //new Oid("1.2.840.113549.1.7.1", "data"), encryptedMessageData);
        var signedCms = new SignedCms(contentInfo, false);
        signedCms.ComputeSignature(signer);

        return signedCms.Encode();
    }

    private X509Certificate2 getSignerCert()
    {
        string thumbprint = "CACertThumbprint";

        thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper();

        // Get a local private key for sigining the outer envelope 
        var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
        certStore.Open(OpenFlags.ReadOnly);

        var signerCert = certStore
            .Certificates
            .Find(X509FindType.FindByThumbprint, thumbprint, false)
            .Cast<X509Certificate2>()
            .Single();

        certStore.Close();
        return signerCert;
    }

    //  Decrypt the encoded EnvelopedCms message for one of the
    //  recipients.
    public Byte[] DecryptMsg(byte[] encodedEnvelopedCms, X509Certificate2Collection caChain)
    {
        //  Prepare object in which to decode and decrypt.
        EnvelopedCms envelopedCms = new EnvelopedCms();

        //  Decode the message.
        envelopedCms.Decode(encodedEnvelopedCms);

        //  Display the number of recipients
        DisplayEnvelopedCms(envelopedCms, false);

        //  Decrypt the message.
        this.log.Debug("Decrypting Data for one recipient ... ");
        envelopedCms.Decrypt(envelopedCms.RecipientInfos[0], caChain);
        this.log.Debug("Done.");

        //  The decrypted message occupies the ContentInfo property
        //  after the Decrypt method is invoked.
        return envelopedCms.ContentInfo.Content;
    }

    //  Display the ContentInfo property of an EnvelopedCms object.
    private void DisplayEnvelopedCmsContent(String desc,
        EnvelopedCms envelopedCms)
    {
        this.log.Debug(string.Format(desc + " (length {0}):  ",
            envelopedCms.ContentInfo.Content.Length));
        foreach (byte b in envelopedCms.ContentInfo.Content)
        {
            this.log.Debug(b.ToString() + " ");
        }
    }

    //  Display some properties of an EnvelopedCms object.
    private void DisplayEnvelopedCms(EnvelopedCms e,
        Boolean displayContent)
    {
        this.log.Debug("\nEnveloped PKCS #7 Message Information:");
        this.log.Debug(string.Format(
            "\tThe number of recipients for the Enveloped PKCS #7 " +
            "is:  {0}", e.RecipientInfos.Count));
        for (int i = 0; i < e.RecipientInfos.Count; i++)
        {
            this.log.Debug(string.Format(
                "\tRecipient #{0} has type {1}.",
                i + 1,
                e.RecipientInfos[i].RecipientIdentifier.Type));
        }
        if (displayContent)
        {
            DisplayEnvelopedCmsContent("Enveloped PKCS #7 Content", e);
        }

    }
}
Airway answered 15/9, 2016 at 11:24 Comment(9)
please let me know if You find anything useful.Birdwatcher
Having done this commercially, i have to say it wasn't easy. Basically, most MDM clients use IETF draft 07. You can skip the getcacaps operation (return 200 OK). For GetCaCert it is just a basic pkcs7 envelope with no data and all CA/RA certs. You will need a pair of RA certs, one to sign and one to encrypt, decrypt the device envelope with the private key of the RA cert, then validate the nonce, issue the cert, and package it back signing with the second RA cert.Sucy
@Sucy thanks for your reply. Yeah, it is a pain to implement! The only source of validation, that it works, is the iOS device accepting the end Certificate. I still haven't made it work, but it seams like the part i am messing up is creating the signed pkcs7. Also I am only responding with the CA cert in the GetCaCert.Airway
@Airway You do need all three certs in the GetCaCert. Do you have the traffic captured? do you see device going all the way to PKIOperation? did you manage to extract the nonce and separate the CSR from the SCEP request?Sucy
@Sucy Yes i am receiving the POST PKIOperation. I use the CSR to generate a new certificate, then I create a pkcs7 envelope, i add the newly created certificate as message data, and encrypt it. I extract the sender nounce, and add it, along with the other needed attributes (messageType, transactionId, recipientNonce, pkiStatus) to the pkcs7, and then encode the envelope, before returning it to the SCEP client.Airway
why are you encrypting the certificate though? From what i can recall it was just a SignedCms with all attributes placed correctly.Sucy
oh sorry, i am not encrypting the SignedCms. I am encrypting the MessageData, before adding it to the SignedCms. But i am not sure if i should do this or if i should, how it should be encrypted.Airway
In the method createEnvelopedDataPkcs7 you have this line var envelopedContent = new System.Security.Cryptography.Pkcs.ContentInfo(new Oid(Oids.Pkcs7.EncryptedData, "envelopedData"), pkcs7RequestData);. Afaic this will not work, because you pass the selfSignedCert.GetEncoded() to it. You need to wrap the certificate in the PKCS7 structure (like a p7b file) without signature, encryption or address. So it's kind of a plain certificate in this particular format.Sucy
@Airway Thanks for this code. It has turned out to be of great help to me as I am new to the field of cryptography. Could you please guide me in writing function DerEncoding.EncodePrintableString/EncodeOctet or point me in right direction?Tirzah

© 2022 - 2024 — McMap. All rights reserved.