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:
- JSCEP, a java library for scep server implementation https://github.com/jscep/jscep
- Bouncy Castle (complicated, and not a lot of documentation on the C# side)
- 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);
}
}
}
createEnvelopedDataPkcs7
you have this linevar envelopedContent = new System.Security.Cryptography.Pkcs.ContentInfo(new Oid(Oids.Pkcs7.EncryptedData, "envelopedData"), pkcs7RequestData);
. Afaic this will not work, because you pass theselfSignedCert.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