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.Crypto.Parameters; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; 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 static readonly CmsSignedHelper Helper = CmsSignedHelper.Instance; private readonly IList _signerInfs = new List(); private readonly HashSet _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 DigestAndSignerInfoGeneratorHolder { internal readonly ISignerInfoGenerator signerInf; internal readonly string digestOID; internal DigestAndSignerInfoGeneratorHolder(ISignerInfoGenerator signerInf, string digestOID) { this.signerInf = signerInf; this.digestOID = digestOID; } internal AlgorithmIdentifier DigestAlgorithm { get { return new AlgorithmIdentifier(new DerObjectIdentifier(this.digestOID), DerNull.Instance); } } } private class SignerInfoGeneratorImpl : ISignerInfoGenerator { private readonly CmsSignedDataStreamGenerator outer; private readonly SignerIdentifier _signerIdentifier; private readonly string _digestOID; private readonly string _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, string digestOID, string encOID, CmsAttributeTableGenerator sAttr, CmsAttributeTableGenerator unsAttr) { this.outer = outer; _signerIdentifier = signerIdentifier; _digestOID = digestOID; _encOID = encOID; _sAttr = sAttr; _unsAttr = unsAttr; _encName = Helper.GetEncryptionAlgName(_encOID); string digestName = Helper.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."); } } } public SignerInfo Generate(DerObjectIdentifier contentType, AlgorithmIdentifier digestAlgorithm, byte[] calculatedDigest) { try { string digestName = Helper.GetDigestAlgName(_digestOID); string signatureName = digestName + "with" + _encName; // AlgorithmIdentifier digAlgId = DigestAlgorithmID; // // byte[] hash = (byte[])outer._messageHashes[Helper.GetDigestAlgName(this._digestOID)]; // outer._digests[_digestOID] = hash.Clone(); 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(Collections.unmodifiableMap(parameters)); Asn1.Cms.AttributeTable signed = _sAttr.GetAttributes(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(Collections.unmodifiableMap(parameters)); Asn1.Cms.AttributeTable unsigned = _unsAttr.GetAttributes(parameters); unsignedAttr = outer.GetAttributeSet(unsigned); } // TODO[RSAPSS] Need the ability to specify non-default parameters Asn1Encodable sigX509Parameters = SignerUtilities.GetDefaultX509Parameters(signatureName); AlgorithmIdentifier digestEncryptionAlgorithm = Helper.GetEncAlgorithmIdentifier( new DerObjectIdentifier(_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(digestOid); } } public void AddDigests(IEnumerable digestOids) { foreach (string digestOid in digestOids) { ConfigureDigest(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, Helper.GetEncOid(privateKey, digestOid), digestOid, signedAttrGenerator, unsignedAttrGenerator); } public void AddSigner( AsymmetricKeyParameter privateKey, X509Certificate cert, string encryptionOid, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { DoAddSigner(privateKey, GetSignerIdentifier(cert), encryptionOid, 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, Helper.GetEncOid(privateKey, digestOid), digestOid, signedAttrGenerator, unsignedAttrGenerator); } public void AddSigner( AsymmetricKeyParameter privateKey, byte[] subjectKeyID, string encryptionOid, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { DoAddSigner(privateKey, GetSignerIdentifier(subjectKeyID), encryptionOid, digestOid, signedAttrGenerator, unsignedAttrGenerator); } private void DoAddSigner( AsymmetricKeyParameter privateKey, SignerIdentifier signerIdentifier, string encryptionOid, string digestOid, CmsAttributeTableGenerator signedAttrGenerator, CmsAttributeTableGenerator unsignedAttrGenerator) { ConfigureDigest(digestOid); SignerInfoGeneratorImpl signerInf = new SignerInfoGeneratorImpl(this, privateKey, signerIdentifier, digestOid, encryptionOid, signedAttrGenerator, unsignedAttrGenerator); _signerInfs.Add(new DigestAndSignerInfoGeneratorHolder(signerInf, digestOid)); } 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.Id); } /** * 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(_messageDigestOids.Count); foreach (string digestOid in _messageDigestOids) { digestAlgs.Add(new AlgorithmIdentifier(new DerObjectIdentifier(digestOid), DerNull.Instance)); } new DerSet(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( string digestOid) { if (_messageDigestsLocked) { if (!_messageDigestOids.Contains(digestOid)) throw new InvalidOperationException("Cannot register new digest OIDs after the data stream is opened"); } else { _messageDigestOids.Add(digestOid); } } private void ConfigureDigest(string digestOid) { RegisterDigestOid(digestOid); string digestName = Helper.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] = Helper.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 (DigestAndSignerInfoGeneratorHolder holder in outer._signerInfs) { AlgorithmIdentifier digestAlgorithm = holder.DigestAlgorithm; byte[] calculatedDigest = outer.m_messageHashes[ Helper.GetDigestAlgName(holder.digestOID)]; outer.m_digests[holder.digestOID] = (byte[])calculatedDigest.Clone(); signerInfos.Add(holder.signerInf.Generate(_contentOID, digestAlgorithm, 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, new DerSet(signerInfos)); _sigGen.Dispose(); _sGen.Dispose(); } private static void WriteToGenerator(Asn1Generator ag, Asn1Encodable ae) { ae.EncodeTo(ag.GetRawOutputStream()); } } } }