| package org.bouncycastle.cms; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.bouncycastle.asn1.ASN1Encodable; |
| import org.bouncycastle.asn1.ASN1EncodableVector; |
| import org.bouncycastle.asn1.ASN1Generator; |
| import org.bouncycastle.asn1.ASN1ObjectIdentifier; |
| import org.bouncycastle.asn1.ASN1OctetStringParser; |
| import org.bouncycastle.asn1.ASN1SequenceParser; |
| import org.bouncycastle.asn1.ASN1Set; |
| import org.bouncycastle.asn1.ASN1SetParser; |
| import org.bouncycastle.asn1.ASN1StreamParser; |
| import org.bouncycastle.asn1.BERSequenceGenerator; |
| import org.bouncycastle.asn1.BERSetParser; |
| import org.bouncycastle.asn1.BERTaggedObject; |
| import org.bouncycastle.asn1.BERTags; |
| import org.bouncycastle.asn1.DERSet; |
| import org.bouncycastle.asn1.DERTaggedObject; |
| import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; |
| import org.bouncycastle.asn1.cms.ContentInfoParser; |
| import org.bouncycastle.asn1.cms.SignedDataParser; |
| import org.bouncycastle.asn1.cms.SignerInfo; |
| import org.bouncycastle.asn1.x509.AlgorithmIdentifier; |
| import org.bouncycastle.operator.DigestCalculator; |
| import org.bouncycastle.operator.DigestCalculatorProvider; |
| import org.bouncycastle.operator.OperatorCreationException; |
| import org.bouncycastle.util.Store; |
| import org.bouncycastle.util.io.Streams; |
| |
| /** |
| * Parsing class for an CMS Signed Data object from an input stream. |
| * <p> |
| * Note: that because we are in a streaming mode only one signer can be tried and it is important |
| * that the methods on the parser are called in the appropriate order. |
| * </p> |
| * <p> |
| * A simple example of usage for an encapsulated signature. |
| * </p> |
| * <p> |
| * Two notes: first, in the example below the validity of |
| * the certificate isn't verified, just the fact that one of the certs |
| * matches the given signer, and, second, because we are in a streaming |
| * mode the order of the operations is important. |
| * </p> |
| * <pre> |
| * CMSSignedDataParser sp = new CMSSignedDataParser(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build(), encapSigData); |
| * |
| * sp.getSignedContent().drain(); |
| * |
| * Store certStore = sp.getCertificates(); |
| * SignerInformationStore signers = sp.getSignerInfos(); |
| * |
| * Collection c = signers.getSigners(); |
| * Iterator it = c.iterator(); |
| * |
| * while (it.hasNext()) |
| * { |
| * SignerInformation signer = (SignerInformation)it.next(); |
| * Collection certCollection = certStore.getMatches(signer.getSID()); |
| * |
| * Iterator certIt = certCollection.iterator(); |
| * X509CertificateHolder cert = (X509CertificateHolder)certIt.next(); |
| * |
| * System.out.println("verify returns: " + signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert))); |
| * } |
| * </pre> |
| * Note also: this class does not introduce buffering - if you are processing large files you should create |
| * the parser with: |
| * <pre> |
| * CMSSignedDataParser ep = new CMSSignedDataParser(new BufferedInputStream(encapSigData, bufSize)); |
| * </pre> |
| * where bufSize is a suitably large buffer size. |
| */ |
| public class CMSSignedDataParser |
| extends CMSContentInfoParser |
| { |
| private static final CMSSignedHelper HELPER = CMSSignedHelper.INSTANCE; |
| |
| private SignedDataParser _signedData; |
| private ASN1ObjectIdentifier _signedContentType; |
| private CMSTypedStream _signedContent; |
| private Map digests; |
| private Set<AlgorithmIdentifier> digestAlgorithms; |
| |
| private SignerInformationStore _signerInfoStore; |
| private ASN1Set _certSet, _crlSet; |
| private boolean _isCertCrlParsed; |
| |
| public CMSSignedDataParser( |
| DigestCalculatorProvider digestCalculatorProvider, |
| byte[] sigBlock) |
| throws CMSException |
| { |
| this(digestCalculatorProvider, new ByteArrayInputStream(sigBlock)); |
| } |
| |
| public CMSSignedDataParser( |
| DigestCalculatorProvider digestCalculatorProvider, |
| CMSTypedStream signedContent, |
| byte[] sigBlock) |
| throws CMSException |
| { |
| this(digestCalculatorProvider, signedContent, new ByteArrayInputStream(sigBlock)); |
| } |
| |
| /** |
| * base constructor - with encapsulated content |
| */ |
| public CMSSignedDataParser( |
| DigestCalculatorProvider digestCalculatorProvider, |
| InputStream sigData) |
| throws CMSException |
| { |
| this(digestCalculatorProvider, null, sigData); |
| } |
| |
| /** |
| * base constructor |
| * |
| * @param digestCalculatorProvider for generating accumulating digests |
| * @param signedContent the content that was signed. |
| * @param sigData the signature object stream. |
| */ |
| public CMSSignedDataParser( |
| DigestCalculatorProvider digestCalculatorProvider, |
| CMSTypedStream signedContent, |
| InputStream sigData) |
| throws CMSException |
| { |
| super(sigData); |
| |
| try |
| { |
| _signedContent = signedContent; |
| _signedData = SignedDataParser.getInstance(_contentInfo.getContent(BERTags.SEQUENCE)); |
| digests = new HashMap(); |
| |
| ASN1SetParser digAlgs = _signedData.getDigestAlgorithms(); |
| ASN1Encodable o; |
| |
| Set<AlgorithmIdentifier> algSet = new HashSet<AlgorithmIdentifier>(); |
| |
| while ((o = digAlgs.readObject()) != null) |
| { |
| AlgorithmIdentifier algId = AlgorithmIdentifier.getInstance(o); |
| |
| algSet.add(algId); |
| |
| try |
| { |
| DigestCalculator calculator = digestCalculatorProvider.get(algId); |
| |
| if (calculator != null) |
| { |
| this.digests.put(algId.getAlgorithm(), calculator); |
| } |
| } |
| catch (OperatorCreationException e) |
| { |
| // ignore |
| } |
| } |
| |
| digestAlgorithms = Collections.unmodifiableSet(algSet); |
| |
| // |
| // If the message is simply a certificate chain message getContent() may return null. |
| // |
| ContentInfoParser cont = _signedData.getEncapContentInfo(); |
| ASN1Encodable contentParser = cont.getContent(BERTags.OCTET_STRING); |
| |
| if (contentParser instanceof ASN1OctetStringParser) |
| { |
| ASN1OctetStringParser octs = (ASN1OctetStringParser)contentParser; |
| |
| CMSTypedStream ctStr = new CMSTypedStream( |
| cont.getContentType(), octs.getOctetStream()); |
| |
| if (_signedContent == null) |
| { |
| _signedContent = ctStr; |
| } |
| else |
| { |
| // |
| // content passed in, need to read past empty encapsulated content info object if present |
| // |
| ctStr.drain(); |
| } |
| } |
| else if (contentParser != null) |
| { |
| PKCS7TypedStream pkcs7Stream = new PKCS7TypedStream(cont.getContentType(), contentParser); |
| |
| if (_signedContent == null) |
| { |
| _signedContent = pkcs7Stream; |
| } |
| else |
| { |
| // |
| // content passed in, need to read past empty encapsulated content info object if present |
| // |
| pkcs7Stream.drain(); |
| } |
| } |
| |
| if (signedContent == null) |
| { |
| _signedContentType = cont.getContentType(); |
| } |
| else |
| { |
| _signedContentType = _signedContent.getContentType(); |
| } |
| } |
| catch (IOException e) |
| { |
| throw new CMSException("io exception: " + e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Return the version number for the SignedData object |
| * |
| * @return the version number |
| */ |
| public int getVersion() |
| { |
| return _signedData.getVersion().getValue().intValue(); |
| } |
| |
| /** |
| * Return the digest algorithm identifiers for the SignedData object |
| * |
| * @return the set of digest algorithm identifiers |
| */ |
| public Set<AlgorithmIdentifier> getDigestAlgorithmIDs() |
| { |
| return digestAlgorithms; |
| } |
| |
| /** |
| * return the collection of signers that are associated with the |
| * signatures for the message. |
| * @throws CMSException |
| */ |
| public SignerInformationStore getSignerInfos() |
| throws CMSException |
| { |
| if (_signerInfoStore == null) |
| { |
| populateCertCrlSets(); |
| |
| List signerInfos = new ArrayList(); |
| Map hashes = new HashMap(); |
| |
| Iterator it = digests.keySet().iterator(); |
| while (it.hasNext()) |
| { |
| Object digestKey = it.next(); |
| |
| hashes.put(digestKey, ((DigestCalculator)digests.get(digestKey)).getDigest()); |
| } |
| |
| try |
| { |
| ASN1SetParser s = _signedData.getSignerInfos(); |
| ASN1Encodable o; |
| |
| while ((o = s.readObject()) != null) |
| { |
| SignerInfo info = SignerInfo.getInstance(o.toASN1Primitive()); |
| |
| byte[] hash = (byte[])hashes.get(info.getDigestAlgorithm().getAlgorithm()); |
| |
| signerInfos.add(new SignerInformation(info, _signedContentType, null, hash)); |
| } |
| } |
| catch (IOException e) |
| { |
| throw new CMSException("io exception: " + e.getMessage(), e); |
| } |
| |
| _signerInfoStore = new SignerInformationStore(signerInfos); |
| } |
| |
| return _signerInfoStore; |
| } |
| |
| /** |
| * Return any X.509 certificate objects in this SignedData structure as a Store of X509CertificateHolder objects. |
| * |
| * @return a Store of X509CertificateHolder objects. |
| */ |
| public Store getCertificates() |
| throws CMSException |
| { |
| populateCertCrlSets(); |
| |
| return HELPER.getCertificates(_certSet); |
| } |
| |
| /** |
| * Return any X.509 CRL objects in this SignedData structure as a Store of X509CRLHolder objects. |
| * |
| * @return a Store of X509CRLHolder objects. |
| */ |
| public Store getCRLs() |
| throws CMSException |
| { |
| populateCertCrlSets(); |
| |
| return HELPER.getCRLs(_crlSet); |
| } |
| |
| /** |
| * Return any X.509 attribute certificate objects in this SignedData structure as a Store of X509AttributeCertificateHolder objects. |
| * |
| * @return a Store of X509AttributeCertificateHolder objects. |
| */ |
| public Store getAttributeCertificates() |
| throws CMSException |
| { |
| populateCertCrlSets(); |
| |
| return HELPER.getAttributeCertificates(_certSet); |
| } |
| |
| /** |
| * Return any OtherRevocationInfo OtherRevInfo objects of the type indicated by otherRevocationInfoFormat in |
| * this SignedData structure. |
| * |
| * @param otherRevocationInfoFormat OID of the format type been looked for. |
| * |
| * @return a Store of ASN1Encodable objects representing any objects of otherRevocationInfoFormat found. |
| */ |
| public Store getOtherRevocationInfo(ASN1ObjectIdentifier otherRevocationInfoFormat) |
| throws CMSException |
| { |
| populateCertCrlSets(); |
| |
| return HELPER.getOtherRevocationInfo(otherRevocationInfoFormat, _crlSet); |
| } |
| |
| private void populateCertCrlSets() |
| throws CMSException |
| { |
| if (_isCertCrlParsed) |
| { |
| return; |
| } |
| |
| _isCertCrlParsed = true; |
| |
| try |
| { |
| // care! Streaming - these must be done in exactly this order. |
| _certSet = getASN1Set(_signedData.getCertificates()); |
| _crlSet = getASN1Set(_signedData.getCrls()); |
| } |
| catch (IOException e) |
| { |
| throw new CMSException("problem parsing cert/crl sets", e); |
| } |
| } |
| |
| /** |
| * Return the a string representation of the OID associated with the |
| * encapsulated content info structure carried in the signed data. |
| * |
| * @return the OID for the content type. |
| */ |
| public String getSignedContentTypeOID() |
| { |
| return _signedContentType.getId(); |
| } |
| |
| public CMSTypedStream getSignedContent() |
| { |
| if (_signedContent == null) |
| { |
| return null; |
| } |
| |
| InputStream digStream = CMSUtils.attachDigestsToInputStream( |
| digests.values(), _signedContent.getContentStream()); |
| |
| return new CMSTypedStream(_signedContent.getContentType(), digStream); |
| } |
| |
| /** |
| * Replace the signerinformation store associated with the passed |
| * in message contained in the stream original with the new one passed in. |
| * You would probably only want to do this if you wanted to change the unsigned |
| * attributes associated with a signer, or perhaps delete one. |
| * <p> |
| * The output stream is returned unclosed. |
| * </p> |
| * @param original the signed data stream to be used as a base. |
| * @param signerInformationStore the new signer information store to use. |
| * @param out the stream to write the new signed data object to. |
| * @return out. |
| */ |
| public static OutputStream replaceSigners( |
| InputStream original, |
| SignerInformationStore signerInformationStore, |
| OutputStream out) |
| throws CMSException, IOException |
| { |
| ASN1StreamParser in = new ASN1StreamParser(original); |
| ContentInfoParser contentInfo = new ContentInfoParser((ASN1SequenceParser)in.readObject()); |
| SignedDataParser signedData = SignedDataParser.getInstance(contentInfo.getContent(BERTags.SEQUENCE)); |
| |
| BERSequenceGenerator sGen = new BERSequenceGenerator(out); |
| |
| sGen.addObject(CMSObjectIdentifiers.signedData); |
| |
| BERSequenceGenerator sigGen = new BERSequenceGenerator(sGen.getRawOutputStream(), 0, true); |
| |
| // version number |
| sigGen.addObject(signedData.getVersion()); |
| |
| // digests |
| signedData.getDigestAlgorithms().toASN1Primitive(); // skip old ones |
| |
| ASN1EncodableVector digestAlgs = new ASN1EncodableVector(); |
| |
| for (Iterator it = signerInformationStore.getSigners().iterator(); it.hasNext();) |
| { |
| SignerInformation signer = (SignerInformation)it.next(); |
| digestAlgs.add(CMSSignedHelper.INSTANCE.fixAlgID(signer.getDigestAlgorithmID())); |
| } |
| |
| sigGen.getRawOutputStream().write(new DERSet(digestAlgs).getEncoded()); |
| |
| // encap content info |
| ContentInfoParser encapContentInfo = signedData.getEncapContentInfo(); |
| |
| BERSequenceGenerator eiGen = new BERSequenceGenerator(sigGen.getRawOutputStream()); |
| |
| eiGen.addObject(encapContentInfo.getContentType()); |
| |
| pipeEncapsulatedOctetString(encapContentInfo, eiGen.getRawOutputStream()); |
| |
| eiGen.close(); |
| |
| |
| writeSetToGeneratorTagged(sigGen, signedData.getCertificates(), 0); |
| writeSetToGeneratorTagged(sigGen, signedData.getCrls(), 1); |
| |
| |
| ASN1EncodableVector signerInfos = new ASN1EncodableVector(); |
| for (Iterator it = signerInformationStore.getSigners().iterator(); it.hasNext();) |
| { |
| SignerInformation signer = (SignerInformation)it.next(); |
| |
| signerInfos.add(signer.toASN1Structure()); |
| } |
| |
| sigGen.getRawOutputStream().write(new DERSet(signerInfos).getEncoded()); |
| |
| sigGen.close(); |
| |
| sGen.close(); |
| |
| return out; |
| } |
| |
| /** |
| * Replace the certificate and CRL information associated with this |
| * CMSSignedData object with the new one passed in. |
| * <p> |
| * The output stream is returned unclosed. |
| * </p> |
| * @param original the signed data stream to be used as a base. |
| * @param certs new certificates to be used, if any. |
| * @param crls new CRLs to be used, if any. |
| * @param attrCerts new attribute certificates to be used, if any. |
| * @param out the stream to write the new signed data object to. |
| * @return out. |
| * @exception CMSException if there is an error processing the CertStore |
| */ |
| public static OutputStream replaceCertificatesAndCRLs( |
| InputStream original, |
| Store certs, |
| Store crls, |
| Store attrCerts, |
| OutputStream out) |
| throws CMSException, IOException |
| { |
| ASN1StreamParser in = new ASN1StreamParser(original); |
| ContentInfoParser contentInfo = new ContentInfoParser((ASN1SequenceParser)in.readObject()); |
| SignedDataParser signedData = SignedDataParser.getInstance(contentInfo.getContent(BERTags.SEQUENCE)); |
| |
| BERSequenceGenerator sGen = new BERSequenceGenerator(out); |
| |
| sGen.addObject(CMSObjectIdentifiers.signedData); |
| |
| BERSequenceGenerator sigGen = new BERSequenceGenerator(sGen.getRawOutputStream(), 0, true); |
| |
| // version number |
| sigGen.addObject(signedData.getVersion()); |
| |
| // digests |
| sigGen.getRawOutputStream().write(signedData.getDigestAlgorithms().toASN1Primitive().getEncoded()); |
| |
| // encap content info |
| ContentInfoParser encapContentInfo = signedData.getEncapContentInfo(); |
| |
| BERSequenceGenerator eiGen = new BERSequenceGenerator(sigGen.getRawOutputStream()); |
| |
| eiGen.addObject(encapContentInfo.getContentType()); |
| |
| pipeEncapsulatedOctetString(encapContentInfo, eiGen.getRawOutputStream()); |
| |
| eiGen.close(); |
| |
| // |
| // skip existing certs and CRLs |
| // |
| getASN1Set(signedData.getCertificates()); |
| getASN1Set(signedData.getCrls()); |
| |
| // |
| // replace the certs and crls in the SignedData object |
| // |
| if (certs != null || attrCerts != null) |
| { |
| List certificates = new ArrayList(); |
| |
| if (certs != null) |
| { |
| certificates.addAll(CMSUtils.getCertificatesFromStore(certs)); |
| } |
| if (attrCerts != null) |
| { |
| certificates.addAll(CMSUtils.getAttributeCertificatesFromStore(attrCerts)); |
| } |
| |
| ASN1Set asn1Certs = CMSUtils.createBerSetFromList(certificates); |
| |
| if (asn1Certs.size() > 0) |
| { |
| sigGen.getRawOutputStream().write(new DERTaggedObject(false, 0, asn1Certs).getEncoded()); |
| } |
| } |
| |
| if (crls != null) |
| { |
| ASN1Set asn1Crls = CMSUtils.createBerSetFromList(CMSUtils.getCRLsFromStore(crls)); |
| |
| if (asn1Crls.size() > 0) |
| { |
| sigGen.getRawOutputStream().write(new DERTaggedObject(false, 1, asn1Crls).getEncoded()); |
| } |
| } |
| |
| sigGen.getRawOutputStream().write(signedData.getSignerInfos().toASN1Primitive().getEncoded()); |
| |
| sigGen.close(); |
| |
| sGen.close(); |
| |
| return out; |
| } |
| |
| private static void writeSetToGeneratorTagged( |
| ASN1Generator asn1Gen, |
| ASN1SetParser asn1SetParser, |
| int tagNo) |
| throws IOException |
| { |
| ASN1Set asn1Set = getASN1Set(asn1SetParser); |
| |
| if (asn1Set != null) |
| { |
| if (asn1SetParser instanceof BERSetParser) |
| { |
| asn1Gen.getRawOutputStream().write(new BERTaggedObject(false, tagNo, asn1Set).getEncoded()); |
| } |
| else |
| { |
| asn1Gen.getRawOutputStream().write(new DERTaggedObject(false, tagNo, asn1Set).getEncoded()); |
| } |
| } |
| } |
| |
| private static ASN1Set getASN1Set( |
| ASN1SetParser asn1SetParser) |
| { |
| return asn1SetParser == null |
| ? null |
| : ASN1Set.getInstance(asn1SetParser.toASN1Primitive()); |
| } |
| |
| private static void pipeEncapsulatedOctetString(ContentInfoParser encapContentInfo, |
| OutputStream rawOutputStream) throws IOException |
| { |
| ASN1OctetStringParser octs = (ASN1OctetStringParser) |
| encapContentInfo.getContent(BERTags.OCTET_STRING); |
| |
| if (octs != null) |
| { |
| pipeOctetString(octs, rawOutputStream); |
| } |
| |
| // BERTaggedObjectParser contentObject = (BERTaggedObjectParser)encapContentInfo.getContentObject(); |
| // if (contentObject != null) |
| // { |
| // // Handle IndefiniteLengthInputStream safely |
| // InputStream input = ASN1StreamParser.getSafeRawInputStream(contentObject.getContentStream(true)); |
| // |
| // // TODO BerTaggedObjectGenerator? |
| // BEROutputStream berOut = new BEROutputStream(rawOutputStream); |
| // berOut.write(DERTags.CONSTRUCTED | DERTags.TAGGED | 0); |
| // berOut.write(0x80); |
| // |
| // pipeRawOctetString(input, rawOutputStream); |
| // |
| // berOut.write(0x00); |
| // berOut.write(0x00); |
| // |
| // input.close(); |
| // } |
| } |
| |
| private static void pipeOctetString( |
| ASN1OctetStringParser octs, |
| OutputStream output) |
| throws IOException |
| { |
| // TODO Allow specification of a specific fragment size? |
| OutputStream outOctets = CMSUtils.createBEROctetOutputStream( |
| output, 0, true, 0); |
| Streams.pipeAll(octs.getOctetStream(), outOctets); |
| outOctets.close(); |
| } |
| |
| // private static void pipeRawOctetString( |
| // InputStream rawInput, |
| // OutputStream rawOutput) |
| // throws IOException |
| // { |
| // InputStream tee = new TeeInputStream(rawInput, rawOutput); |
| // ASN1StreamParser sp = new ASN1StreamParser(tee); |
| // ASN1OctetStringParser octs = (ASN1OctetStringParser)sp.readObject(); |
| // Streams.drain(octs.getOctetStream()); |
| // } |
| } |