using System; using System.Collections.Generic; using System.IO; using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.Cms; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.IO; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities.Collections; using Org.BouncyCastle.Utilities.IO; using Org.BouncyCastle.X509; namespace Org.BouncyCastle.Cms { /** * General class for generating a pkcs7-signature message stream. *

* A simple example of usage. *

*
    *      IX509Store                   certs...
    *      CmsSignedDataStreamGenerator gen = new CmsSignedDataStreamGenerator();
    *
    *      gen.AddSigner(privateKey, cert, CmsSignedDataStreamGenerator.DIGEST_SHA1);
    *
    *      gen.AddCertificates(certs);
    *
    *      Stream sigOut = gen.Open(bOut);
    *
    *      sigOut.Write(Encoding.UTF8.GetBytes("Hello World!"));
    *
    *      sigOut.Close();
    * 
*/ public class CmsSignedDataStreamGenerator : CmsSignedGenerator { private readonly List m_signerInfoGens = new List(); private readonly HashSet m_messageDigestOids = new HashSet(); private readonly IDictionary m_messageDigests = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly IDictionary m_messageHashes = new Dictionary(StringComparer.OrdinalIgnoreCase); private bool _messageDigestsLocked; private int _bufferSize; private class SignerInfoGeneratorImpl { private readonly CmsSignedDataStreamGenerator outer; private readonly SignerIdentifier _signerIdentifier; internal readonly DerObjectIdentifier m_digestOid; private readonly DerObjectIdentifier m_encOid; private readonly CmsAttributeTableGenerator _sAttr; private readonly CmsAttributeTableGenerator _unsAttr; private readonly string _encName; private readonly ISigner _sig; internal SignerInfoGeneratorImpl( CmsSignedDataStreamGenerator outer, AsymmetricKeyParameter key, SignerIdentifier signerIdentifier, DerObjectIdentifier digestOid, DerObjectIdentifier encOid, CmsAttributeTableGenerator sAttr, CmsAttributeTableGenerator unsAttr) { this.outer = outer; _signerIdentifier = signerIdentifier; m_digestOid = digestOid; m_encOid = encOid; _sAttr = sAttr; _unsAttr = unsAttr; _encName = CmsSignedHelper.GetEncryptionAlgName(m_encOid); string digestName = CmsSignedHelper.GetDigestAlgName(digestOid); string signatureName = digestName + "with" + _encName; if (_sAttr != null) { _sig = SignerUtilities.InitSigner(signatureName, true, key, outer.m_random); } else { // Note: Need to use raw signatures here since we have already calculated the digest if (_encName.Equals("RSA")) { _sig = SignerUtilities.InitSigner("RSA", true, key, outer.m_random); } else if (_encName.Equals("DSA")) { _sig = SignerUtilities.InitSigner("NONEwithDSA", true, key, outer.m_random); } // TODO Add support for raw PSS // else if (_encName.equals("RSAandMGF1")) // { // _sig = CMSSignedHelper.INSTANCE.getSignatureInstance("NONEWITHRSAPSS", _sigProvider); // try // { // // Init the params this way to avoid having a 'raw' version of each PSS algorithm // Signature sig2 = CMSSignedHelper.INSTANCE.getSignatureInstance(signatureName, _sigProvider); // PSSParameterSpec spec = (PSSParameterSpec)sig2.getParameters().getParameterSpec(PSSParameterSpec.class); // _sig.setParameter(spec); // } // catch (Exception e) // { // throw new SignatureException("algorithm: " + _encName + " could not be configured."); // } // } else { throw new SignatureException("algorithm: " + _encName + " not supported in base signatures."); } } } internal SignerInfo Generate(DerObjectIdentifier contentType, byte[] calculatedDigest) { // TODO AlgorithmIdentifier noParams handling (configure an IDigestAlgorithmFinder) var digestAlgorithm = new AlgorithmIdentifier(m_digestOid, DerNull.Instance); try { string digestName = CmsSignedHelper.GetDigestAlgName(m_digestOid); string signatureName = digestName + "with" + _encName; byte[] bytesToSign = calculatedDigest; /* RFC 3852 5.4 * The result of the message digest calculation process depends on * whether the signedAttrs field is present. When the field is absent, * the result is just the message digest of the content as described * * above. When the field is present, however, the result is the message * digest of the complete DER encoding of the SignedAttrs value * contained in the signedAttrs field. */ Asn1Set signedAttr = null; if (_sAttr != null) { var parameters = outer.GetBaseParameters(contentType, digestAlgorithm, calculatedDigest); Asn1.Cms.AttributeTable signed = _sAttr.GetAttributes(CollectionUtilities.ReadOnly(parameters)); if (contentType == null) //counter signature { if (signed != null && signed[CmsAttributes.ContentType] != null) { signed = signed.Remove(CmsAttributes.ContentType); } } signedAttr = outer.GetAttributeSet(signed); // sig must be composed from the DER encoding. bytesToSign = signedAttr.GetEncoded(Asn1Encodable.Der); } else { // Note: Need to use raw signatures here since we have already calculated the digest if (_encName.Equals("RSA")) { DigestInfo dInfo = new DigestInfo(digestAlgorithm, calculatedDigest); bytesToSign = dInfo.GetEncoded(Asn1Encodable.Der); } } _sig.BlockUpdate(bytesToSign, 0, bytesToSign.Length); byte[] sigBytes = _sig.GenerateSignature(); Asn1Set unsignedAttr = null; if (_unsAttr != null) { var parameters = outer.GetBaseParameters(contentType, digestAlgorithm, calculatedDigest); parameters[CmsAttributeTableParameter.Signature] = sigBytes.Clone(); Asn1.Cms.AttributeTable unsigned = _unsAttr.GetAttributes(CollectionUtilities.ReadOnly(parameters)); unsignedAttr = outer.GetAttributeSet(unsigned); } // TODO[RSAPSS] Need the ability to specify non-default parameters Asn1Encodable sigX509Parameters = SignerUtilities.GetDefaultX509Parameters(signatureName); AlgorithmIdentifier digestEncryptionAlgorithm = CmsSignedHelper.GetEncAlgorithmIdentifier( m_encOid, sigX509Parameters); return new SignerInfo(_signerIdentifier, digestAlgorithm, signedAttr, digestEncryptionAlgorithm, new DerOctetString(sigBytes), unsignedAttr); } catch (IOException e) { throw new CmsStreamException("encoding error.", e); } catch (SignatureException e) { throw new CmsStreamException("error creating signature.", e); } } } public CmsSignedDataStreamGenerator() { } /// Constructor allowing specific source of randomness /// Instance of SecureRandom to use. public CmsSignedDataStreamGenerator(SecureRandom random) : base(random) { } /** * Set the underlying string size for encapsulated data * * @param bufferSize length of octet strings to buffer the data. */ public void SetBufferSize(int bufferSize) { _bufferSize = bufferSize; } public void AddDigests(params string[] digestOids) { foreach (string digestOid in digestOids) { ConfigureDigest(new DerObjectIdentifier(digestOid)); } } public void AddDigests(IEnumerable digestOids) { foreach (string digestOid in digestOids) { ConfigureDigest(new DerObjectIdentifier(digestOid)); } } /** * add a signer - no attributes other than the default ones will be * provided here. * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner(AsymmetricKeyParameter privateKey, X509Certificate cert, string digestOid) { AddSigner(privateKey, cert, digestOid, new DefaultSignedAttributeTableGenerator(), null); } /** * add a signer, specifying the digest encryption algorithm - no attributes other than the default ones will be * provided here. * @throws NoSuchProviderException * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner( AsymmetricKeyParameter privateKey, X509Certificate cert, string encryptionOid, string digestOid) { AddSigner(privateKey, cert, encryptionOid, digestOid, new DefaultSignedAttributeTableGenerator(), (CmsAttributeTableGenerator)null); } /** * add a signer with extra signed/unsigned attributes. * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner( AsymmetricKeyParameter privateKey, X509Certificate cert, string digestOid, Asn1.Cms.AttributeTable signedAttr, Asn1.Cms.AttributeTable unsignedAttr) { AddSigner(privateKey, cert, digestOid, new DefaultSignedAttributeTableGenerator(signedAttr), new SimpleAttributeTableGenerator(unsignedAttr)); } /** * add a signer with extra signed/unsigned attributes - specifying digest * encryption algorithm. * @throws NoSuchProviderException * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner( AsymmetricKeyParameter privateKey, X509Certificate cert, string encryptionOid, string digestOid, Asn1.Cms.AttributeTable signedAttr, Asn1.Cms.AttributeTable unsignedAttr) { AddSigner(privateKey, cert, encryptionOid, digestOid, new DefaultSignedAttributeTableGenerator(signedAttr), new SimpleAttributeTableGenerator(unsignedAttr)); } public void AddSigner( AsymmetricKeyParameter privateKey, X509Certificate cert, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { AddSigner(privateKey, cert, CmsSignedHelper.GetEncOid(privateKey, digestOid)?.Id, digestOid, signedAttrGenerator, unsignedAttrGenerator); } public void AddSigner( AsymmetricKeyParameter privateKey, X509Certificate cert, string encryptionOid, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { DoAddSigner(privateKey, GetSignerIdentifier(cert), new DerObjectIdentifier(encryptionOid), new DerObjectIdentifier(digestOid), signedAttrGenerator, unsignedAttrGenerator); } /** * add a signer - no attributes other than the default ones will be * provided here. * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner( AsymmetricKeyParameter privateKey, byte[] subjectKeyID, string digestOid) { AddSigner(privateKey, subjectKeyID, digestOid, new DefaultSignedAttributeTableGenerator(), (CmsAttributeTableGenerator)null); } /** * add a signer - no attributes other than the default ones will be * provided here. * @throws NoSuchProviderException * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner( AsymmetricKeyParameter privateKey, byte[] subjectKeyID, string encryptionOid, string digestOid) { AddSigner(privateKey, subjectKeyID, encryptionOid, digestOid, new DefaultSignedAttributeTableGenerator(), (CmsAttributeTableGenerator)null); } /** * add a signer with extra signed/unsigned attributes. * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public void AddSigner( AsymmetricKeyParameter privateKey, byte[] subjectKeyID, string digestOid, Asn1.Cms.AttributeTable signedAttr, Asn1.Cms.AttributeTable unsignedAttr) { AddSigner(privateKey, subjectKeyID, digestOid, new DefaultSignedAttributeTableGenerator(signedAttr), new SimpleAttributeTableGenerator(unsignedAttr)); } public void AddSigner( AsymmetricKeyParameter privateKey, byte[] subjectKeyID, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { AddSigner(privateKey, subjectKeyID, CmsSignedHelper.GetEncOid(privateKey, digestOid)?.Id, digestOid, signedAttrGenerator, unsignedAttrGenerator); } public void AddSigner( AsymmetricKeyParameter privateKey, byte[] subjectKeyID, string encryptionOid, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { DoAddSigner(privateKey, GetSignerIdentifier(subjectKeyID), new DerObjectIdentifier(encryptionOid), new DerObjectIdentifier(digestOid), signedAttrGenerator, unsignedAttrGenerator); } private void DoAddSigner( AsymmetricKeyParameter privateKey, SignerIdentifier signerIdentifier, DerObjectIdentifier encryptionOid, DerObjectIdentifier digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { ConfigureDigest(digestOid); SignerInfoGeneratorImpl signerInfoGen = new SignerInfoGeneratorImpl(this, privateKey, signerIdentifier, digestOid, encryptionOid, signedAttrGenerator, unsignedAttrGenerator); m_signerInfoGens.Add(signerInfoGen); } internal override void AddSignerCallback( SignerInformation si) { // FIXME If there were parameters in si.DigestAlgorithmID.Parameters, they are lost // NB: Would need to call FixAlgID on the DigestAlgorithmID // For precalculated signers, just need to register the algorithm, not configure a digest RegisterDigestOid(si.DigestAlgorithmID.Algorithm); } /** * generate a signed object that for a CMS Signed Data object */ public Stream Open( Stream outStream) { return Open(outStream, false); } /** * generate a signed object that for a CMS Signed Data * object - if encapsulate is true a copy * of the message will be included in the signature with the * default content type "data". */ public Stream Open( Stream outStream, bool encapsulate) { return Open(outStream, Data, encapsulate); } /** * generate a signed object that for a CMS Signed Data * object using the given provider - if encapsulate is true a copy * of the message will be included in the signature with the * default content type "data". If dataOutputStream is non null the data * being signed will be written to the stream as it is processed. * @param out stream the CMS object is to be written to. * @param encapsulate true if data should be encapsulated. * @param dataOutputStream output stream to copy the data being signed to. */ public Stream Open( Stream outStream, bool encapsulate, Stream dataOutputStream) { return Open(outStream, Data, encapsulate, dataOutputStream); } /** * generate a signed object that for a CMS Signed Data * object - if encapsulate is true a copy * of the message will be included in the signature. The content type * is set according to the OID represented by the string signedContentType. */ public Stream Open( Stream outStream, string signedContentType, bool encapsulate) { return Open(outStream, signedContentType, encapsulate, null); } /** * generate a signed object that for a CMS Signed Data * object using the given provider - if encapsulate is true a copy * of the message will be included in the signature. The content type * is set according to the OID represented by the string signedContentType. * @param out stream the CMS object is to be written to. * @param signedContentType OID for data to be signed. * @param encapsulate true if data should be encapsulated. * @param dataOutputStream output stream to copy the data being signed to. */ public Stream Open( Stream outStream, string signedContentType, bool encapsulate, Stream dataOutputStream) { if (outStream == null) throw new ArgumentNullException("outStream"); if (!outStream.CanWrite) throw new ArgumentException("Expected writeable stream", "outStream"); if (dataOutputStream != null && !dataOutputStream.CanWrite) throw new ArgumentException("Expected writeable stream", "dataOutputStream"); _messageDigestsLocked = true; // // ContentInfo // BerSequenceGenerator sGen = new BerSequenceGenerator(outStream); sGen.AddObject(CmsObjectIdentifiers.SignedData); // // Signed Data // BerSequenceGenerator sigGen = new BerSequenceGenerator( sGen.GetRawOutputStream(), 0, true); bool isCounterSignature = (signedContentType == null); DerObjectIdentifier contentTypeOid = isCounterSignature ? null : new DerObjectIdentifier(signedContentType); sigGen.AddObject(CalculateVersion(contentTypeOid)); Asn1EncodableVector digestAlgs = new Asn1EncodableVector(m_messageDigestOids.Count); foreach (var messageDigestOid in m_messageDigestOids) { // TODO AlgorithmIdentifier noParams handling (configure an IDigestAlgorithmFinder) digestAlgs.Add(new AlgorithmIdentifier(messageDigestOid, DerNull.Instance)); } DerSet.FromVector(digestAlgs).EncodeTo(sigGen.GetRawOutputStream()); BerSequenceGenerator eiGen = new BerSequenceGenerator(sigGen.GetRawOutputStream()); eiGen.AddObject(contentTypeOid); BerOctetStringGenerator octGen = null; Stream encapStream = null; // If encapsulating, add the data as an octet string in the sequence if (encapsulate) { octGen = new BerOctetStringGenerator(eiGen.GetRawOutputStream(), 0, true); encapStream = octGen.GetOctetOutputStream(_bufferSize); } // Also send the data to 'dataOutputStream' if necessary Stream teeStream = GetSafeTeeOutputStream(dataOutputStream, encapStream); // Let all the digests see the data as it is written Stream digStream = AttachDigestsToOutputStream(m_messageDigests.Values, teeStream); return new CmsSignedDataOutputStream(this, digStream, signedContentType, sGen, sigGen, eiGen, octGen); } private void RegisterDigestOid(DerObjectIdentifier digestOid) { if (!_messageDigestsLocked) { m_messageDigestOids.Add(digestOid); } else if (!m_messageDigestOids.Contains(digestOid)) { throw new InvalidOperationException("Cannot register new digest OIDs after the data stream is opened"); } } private void ConfigureDigest(DerObjectIdentifier digestOid) { RegisterDigestOid(digestOid); string digestName = CmsSignedHelper.GetDigestAlgName(digestOid); if (!m_messageDigests.ContainsKey(digestName)) { if (_messageDigestsLocked) throw new InvalidOperationException("Cannot configure new digests after the data stream is opened"); m_messageDigests[digestName] = CmsSignedHelper.GetDigestInstance(digestName); } } // TODO Make public? internal void Generate( Stream outStream, string eContentType, bool encapsulate, Stream dataOutputStream, CmsProcessable content) { using (var signedOut = Open(outStream, eContentType, encapsulate, dataOutputStream)) { if (content != null) { content.Write(signedOut); } } } // RFC3852, section 5.1: // IF ((certificates is present) AND // (any certificates with a type of other are present)) OR // ((crls is present) AND // (any crls with a type of other are present)) // THEN version MUST be 5 // ELSE // IF (certificates is present) AND // (any version 2 attribute certificates are present) // THEN version MUST be 4 // ELSE // IF ((certificates is present) AND // (any version 1 attribute certificates are present)) OR // (any SignerInfo structures are version 3) OR // (encapContentInfo eContentType is other than id-data) // THEN version MUST be 3 // ELSE version MUST be 1 // private DerInteger CalculateVersion( DerObjectIdentifier contentOid) { bool otherCert = false; bool otherCrl = false; bool attrCertV1Found = false; bool attrCertV2Found = false; if (_certs != null) { foreach (object obj in _certs) { if (obj is Asn1TaggedObject tagged) { if (tagged.TagNo == 1) { attrCertV1Found = true; } else if (tagged.TagNo == 2) { attrCertV2Found = true; } else if (tagged.TagNo == 3) { otherCert = true; break; } } } } if (otherCert) { return new DerInteger(5); } if (_crls != null) { foreach (object obj in _crls) { if (obj is Asn1TaggedObject) { otherCrl = true; break; } } } if (otherCrl) { return new DerInteger(5); } if (attrCertV2Found) { return new DerInteger(4); } if (attrCertV1Found || !CmsObjectIdentifiers.Data.Equals(contentOid) || CheckForVersion3(_signers)) { return new DerInteger(3); } return new DerInteger(1); } private bool CheckForVersion3(IList signerInfos) { foreach (SignerInformation si in signerInfos) { SignerInfo s = SignerInfo.GetInstance(si.ToSignerInfo()); if (s.Version.IntValueExact == 3) return true; } return false; } private static Stream AttachDigestsToOutputStream(IEnumerable digests, Stream s) { Stream result = s; foreach (IDigest digest in digests) { result = GetSafeTeeOutputStream(result, new DigestSink(digest)); } return result; } private static Stream GetSafeOutputStream(Stream s) { return s ?? Stream.Null; } private static Stream GetSafeTeeOutputStream(Stream s1, Stream s2) { if (s1 == null) return GetSafeOutputStream(s2); if (s2 == null) return GetSafeOutputStream(s1); return new TeeOutputStream(s1, s2); } private class CmsSignedDataOutputStream : BaseOutputStream { private readonly CmsSignedDataStreamGenerator outer; private Stream _out; private DerObjectIdentifier _contentOID; private BerSequenceGenerator _sGen; private BerSequenceGenerator _sigGen; private BerSequenceGenerator _eiGen; private BerOctetStringGenerator _octGen; public CmsSignedDataOutputStream( CmsSignedDataStreamGenerator outer, Stream outStream, string contentOID, BerSequenceGenerator sGen, BerSequenceGenerator sigGen, BerSequenceGenerator eiGen, BerOctetStringGenerator octGen) { this.outer = outer; _out = outStream; _contentOID = new DerObjectIdentifier(contentOID); _sGen = sGen; _sigGen = sigGen; _eiGen = eiGen; _octGen = octGen; } public override void Write(byte[] buffer, int offset, int count) { _out.Write(buffer, offset, count); } #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER public override void Write(ReadOnlySpan buffer) { _out.Write(buffer); } #endif public override void WriteByte(byte value) { _out.WriteByte(value); } protected override void Dispose(bool disposing) { if (disposing) { DoClose(); } base.Dispose(disposing); } private void DoClose() { _out.Dispose(); // TODO Parent context(s) should really be be closed explicitly // Only for encapsulation _octGen?.Dispose(); _eiGen.Dispose(); outer.m_digests.Clear(); // clear the current preserved digest state if (outer._certs.Count > 0) { Asn1Set certs = outer.UseDerForCerts ? CmsUtilities.CreateDerSetFromList(outer._certs) : CmsUtilities.CreateBerSetFromList(outer._certs); WriteToGenerator(_sigGen, new BerTaggedObject(false, 0, certs)); } if (outer._crls.Count > 0) { Asn1Set crls = outer.UseDerForCrls ? CmsUtilities.CreateDerSetFromList(outer._crls) : CmsUtilities.CreateBerSetFromList(outer._crls); WriteToGenerator(_sigGen, new BerTaggedObject(false, 1, crls)); } // // Calculate the digest hashes // foreach (var de in outer.m_messageDigests) { outer.m_messageHashes.Add(de.Key, DigestUtilities.DoFinal(de.Value)); } // TODO If the digest OIDs for precalculated signers weren't mixed in with // the others, we could fill in outer._digests here, instead of SignerInfoGenerator.Generate // // collect all the SignerInfo objects // Asn1EncodableVector signerInfos = new Asn1EncodableVector(); // // add the generated SignerInfo objects // foreach (SignerInfoGeneratorImpl signerInfoGen in outer.m_signerInfoGens) { var digestOid = signerInfoGen.m_digestOid; string digestName = CmsSignedHelper.GetDigestAlgName(digestOid); byte[] calculatedDigest = outer.m_messageHashes[digestName]; outer.m_digests[digestOid] = (byte[])calculatedDigest.Clone(); signerInfos.Add(signerInfoGen.Generate(_contentOID, calculatedDigest)); } // // add the precalculated SignerInfo objects. // { foreach (SignerInformation signer in outer._signers) { // TODO Verify the content type and calculated digest match the precalculated SignerInfo // if (!signer.ContentType.Equals(_contentOID)) // { // // TODO The precalculated content type did not match - error? // } // // byte[] calculatedDigest = (byte[])outer._digests[signer.DigestAlgOid]; // if (calculatedDigest == null) // { // // TODO We can't confirm this digest because we didn't calculate it - error? // } // else // { // if (!Arrays.AreEqual(signer.GetContentDigest(), calculatedDigest)) // { // // TODO The precalculated digest did not match - error? // } // } signerInfos.Add(signer.ToSignerInfo()); } } WriteToGenerator(_sigGen, DerSet.FromVector(signerInfos)); _sigGen.Dispose(); _sGen.Dispose(); } private static void WriteToGenerator(Asn1Generator ag, Asn1Encodable ae) { ae.EncodeTo(ag.GetRawOutputStream()); } } } }