|
| 1 | +package org.bouncycastle.cades; |
| 2 | + |
| 3 | +import java.io.IOException; |
| 4 | +import java.io.OutputStream; |
| 5 | +import java.util.ArrayList; |
| 6 | +import java.util.Collection; |
| 7 | +import java.util.Enumeration; |
| 8 | +import java.util.Iterator; |
| 9 | +import java.util.List; |
| 10 | + |
| 11 | +import org.bouncycastle.asn1.ASN1Encodable; |
| 12 | +import org.bouncycastle.asn1.ASN1EncodableVector; |
| 13 | +import org.bouncycastle.asn1.ASN1Encoding; |
| 14 | +import org.bouncycastle.asn1.ASN1ObjectIdentifier; |
| 15 | +import org.bouncycastle.asn1.ASN1OctetString; |
| 16 | +import org.bouncycastle.asn1.ASN1Primitive; |
| 17 | +import org.bouncycastle.asn1.ASN1Set; |
| 18 | +import org.bouncycastle.asn1.DERSet; |
| 19 | +import org.bouncycastle.asn1.cms.Attribute; |
| 20 | +import org.bouncycastle.asn1.cms.AttributeTable; |
| 21 | +import org.bouncycastle.asn1.cms.ContentInfo; |
| 22 | +import org.bouncycastle.asn1.cms.SignedData; |
| 23 | +import org.bouncycastle.asn1.esf.ESFAttributes; |
| 24 | +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; |
| 25 | +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; |
| 26 | +import org.bouncycastle.cms.CMSSignedData; |
| 27 | +import org.bouncycastle.cms.SignerId; |
| 28 | +import org.bouncycastle.cms.SignerInformation; |
| 29 | +import org.bouncycastle.cms.SignerInformationStore; |
| 30 | +import org.bouncycastle.operator.DigestCalculator; |
| 31 | +import org.bouncycastle.operator.DigestCalculatorProvider; |
| 32 | +import org.bouncycastle.operator.OperatorCreationException; |
| 33 | +import org.bouncycastle.tsp.TimeStampToken; |
| 34 | + |
| 35 | +/** |
| 36 | + * Helpers for upgrading a CAdES B-LT signature to B-LTA by attaching an |
| 37 | + * archive-time-stamp covering the entire SignedData. |
| 38 | + * <p> |
| 39 | + * The helper emits the ETSI TS 101 733 v1.7.4 "v2" form |
| 40 | + * ({@code id-aa-ets-archiveTimestampV2}, OID {@code 1.2.840.113549.1.9.16.2.48}). |
| 41 | + * The newer ETSI EN 319 122-1 "v3" form, which embeds an |
| 42 | + * {@code ats-hash-index-v3} signed attribute inside the TSA token, is not |
| 43 | + * yet supported — v3 generation requires custom signed-attribute |
| 44 | + * injection on the TSA token that is not yet exposed by the {@code tsp} |
| 45 | + * module. |
| 46 | + * <p> |
| 47 | + * The v2 imprint is the digest, under the caller-supplied algorithm, of |
| 48 | + * the canonical concatenation defined by ETSI TS 101 733 Annex A: |
| 49 | + * <ol> |
| 50 | + * <li>the content octets of the encapsulated content (omitted if the |
| 51 | + * signed-data is detached or has no eContent),</li> |
| 52 | + * <li>each {@code Certificate} from the {@code certificates} field, in |
| 53 | + * wire-encoding order, as its own DER encoding,</li> |
| 54 | + * <li>each {@code CertificateList} from the {@code crls} field, in |
| 55 | + * wire-encoding order, as its own DER encoding,</li> |
| 56 | + * <li>for each {@code SignerInfo} (in wire-encoding order), its DER |
| 57 | + * encoding with any archive-time-stamp attributes removed from |
| 58 | + * the {@code unsignedAttrs} field.</li> |
| 59 | + * </ol> |
| 60 | + * Typical caller flow: |
| 61 | + * <ol> |
| 62 | + * <li>{@link #computeArchiveTimestampImprint(CMSSignedData, AlgorithmIdentifier, |
| 63 | + * DigestCalculatorProvider)} returns the digest bytes.</li> |
| 64 | + * <li>The caller obtains a {@link TimeStampToken} from a TSA over those |
| 65 | + * bytes (transport is out of scope for BC).</li> |
| 66 | + * <li>{@link #applyArchiveTimestamp(CMSSignedData, SignerId, TimeStampToken)} |
| 67 | + * returns a new CMSSignedData with the token attached as an |
| 68 | + * {@code id-aa-ets-archiveTimestampV2} unsigned attribute on the |
| 69 | + * chosen signer.</li> |
| 70 | + * </ol> |
| 71 | + */ |
| 72 | +public final class CAdESArchiveTimestampUtil |
| 73 | +{ |
| 74 | + /** ETSI TS 101 733 v1.7.4 id-aa-ets-archiveTimestampV2 OID. */ |
| 75 | + public static final ASN1ObjectIdentifier id_aa_ets_archiveTimestampV2 = |
| 76 | + ESFAttributes.archiveTimestampV2; |
| 77 | + |
| 78 | + private CAdESArchiveTimestampUtil() |
| 79 | + { |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * Digest the canonical archive-time-stamp v2 input for {@code signed} |
| 84 | + * under {@code digestAlg}. |
| 85 | + */ |
| 86 | + public static byte[] computeArchiveTimestampImprint(CMSSignedData signed, |
| 87 | + AlgorithmIdentifier digestAlg, |
| 88 | + DigestCalculatorProvider digCalcProv) |
| 89 | + throws CAdESException, OperatorCreationException, IOException |
| 90 | + { |
| 91 | + if (signed == null) |
| 92 | + { |
| 93 | + throw new NullPointerException("signed"); |
| 94 | + } |
| 95 | + if (digestAlg == null) |
| 96 | + { |
| 97 | + throw new NullPointerException("digestAlg"); |
| 98 | + } |
| 99 | + |
| 100 | + DigestCalculator dc = digCalcProv.get(digestAlg); |
| 101 | + OutputStream out = dc.getOutputStream(); |
| 102 | + feedCanonicalInput(signed, out); |
| 103 | + out.close(); |
| 104 | + return dc.getDigest(); |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * Attach an archive-time-stamp v2 to the signer matched by |
| 109 | + * {@code signerId}. If the signer already has one or more |
| 110 | + * archive-timestamp attributes the new token is appended into the |
| 111 | + * existing attribute's value-set; archive-timestamps form an ordered |
| 112 | + * chain so this preserves earlier timestamps. |
| 113 | + */ |
| 114 | + public static CMSSignedData applyArchiveTimestamp(CMSSignedData signed, |
| 115 | + SignerId signerId, |
| 116 | + TimeStampToken token) |
| 117 | + throws CAdESException |
| 118 | + { |
| 119 | + if (signed == null) |
| 120 | + { |
| 121 | + throw new NullPointerException("signed"); |
| 122 | + } |
| 123 | + if (signerId == null) |
| 124 | + { |
| 125 | + throw new NullPointerException("signerId"); |
| 126 | + } |
| 127 | + if (token == null) |
| 128 | + { |
| 129 | + throw new NullPointerException("token"); |
| 130 | + } |
| 131 | + |
| 132 | + SignerInformationStore signers = signed.getSignerInfos(); |
| 133 | + Collection<SignerInformation> matched = signers.getSigners(signerId); |
| 134 | + if (matched.isEmpty()) |
| 135 | + { |
| 136 | + throw new CAdESException("no signer matched in CMSSignedData"); |
| 137 | + } |
| 138 | + |
| 139 | + ContentInfo tokenCi; |
| 140 | + try |
| 141 | + { |
| 142 | + tokenCi = ContentInfo.getInstance( |
| 143 | + ASN1Primitive.fromByteArray(token.getEncoded())); |
| 144 | + } |
| 145 | + catch (IOException e) |
| 146 | + { |
| 147 | + throw new CAdESException("unable to encode TimeStampToken: " + e.getMessage(), e); |
| 148 | + } |
| 149 | + |
| 150 | + List<SignerInformation> rebuilt = new ArrayList<SignerInformation>(signers.size()); |
| 151 | + for (Iterator<SignerInformation> it = signers.getSigners().iterator(); it.hasNext(); ) |
| 152 | + { |
| 153 | + SignerInformation cur = it.next(); |
| 154 | + if (matched.contains(cur)) |
| 155 | + { |
| 156 | + rebuilt.add(appendArchiveTimestamp(cur, tokenCi)); |
| 157 | + } |
| 158 | + else |
| 159 | + { |
| 160 | + rebuilt.add(cur); |
| 161 | + } |
| 162 | + } |
| 163 | + return CMSSignedData.replaceSigners(signed, new SignerInformationStore(rebuilt)); |
| 164 | + } |
| 165 | + |
| 166 | + private static SignerInformation appendArchiveTimestamp(SignerInformation signer, |
| 167 | + ContentInfo tokenCi) |
| 168 | + { |
| 169 | + AttributeTable unsigned = signer.getUnsignedAttributes(); |
| 170 | + |
| 171 | + Attribute existing = unsigned == null |
| 172 | + ? null |
| 173 | + : unsigned.get(id_aa_ets_archiveTimestampV2); |
| 174 | + |
| 175 | + ASN1EncodableVector vals = new ASN1EncodableVector(); |
| 176 | + if (existing != null) |
| 177 | + { |
| 178 | + ASN1Set prev = existing.getAttrValues(); |
| 179 | + for (int i = 0; i != prev.size(); i++) |
| 180 | + { |
| 181 | + vals.add(prev.getObjectAt(i)); |
| 182 | + } |
| 183 | + } |
| 184 | + vals.add(tokenCi); |
| 185 | + Attribute merged = new Attribute(id_aa_ets_archiveTimestampV2, new DERSet(vals)); |
| 186 | + |
| 187 | + ASN1EncodableVector all = unsigned == null |
| 188 | + ? new ASN1EncodableVector() |
| 189 | + : unsigned.toASN1EncodableVector(); |
| 190 | + |
| 191 | + ASN1EncodableVector outVec = new ASN1EncodableVector(); |
| 192 | + for (int i = 0; i != all.size(); ++i) |
| 193 | + { |
| 194 | + Attribute a = Attribute.getInstance(all.get(i)); |
| 195 | + if (!id_aa_ets_archiveTimestampV2.equals(a.getAttrType())) |
| 196 | + { |
| 197 | + outVec.add(a); |
| 198 | + } |
| 199 | + } |
| 200 | + outVec.add(merged); |
| 201 | + |
| 202 | + return SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(outVec)); |
| 203 | + } |
| 204 | + |
| 205 | + /** |
| 206 | + * Walk the SignedData and feed the canonical archive-time-stamp v2 |
| 207 | + * input bytes to {@code out}. |
| 208 | + */ |
| 209 | + private static void feedCanonicalInput(CMSSignedData signed, OutputStream out) |
| 210 | + throws CAdESException, IOException |
| 211 | + { |
| 212 | + SignedData sd; |
| 213 | + try |
| 214 | + { |
| 215 | + sd = SignedData.getInstance(signed.toASN1Structure().getContent()); |
| 216 | + } |
| 217 | + catch (Exception e) |
| 218 | + { |
| 219 | + throw new CAdESException("unable to read SignedData: " + e.getMessage(), |
| 220 | + e instanceof Exception ? (Exception)e : new RuntimeException(e)); |
| 221 | + } |
| 222 | + |
| 223 | + // (1) eContent octets, if present. |
| 224 | + ASN1Encodable encap = sd.getEncapContentInfo().getContent(); |
| 225 | + if (encap instanceof ASN1OctetString) |
| 226 | + { |
| 227 | + out.write(((ASN1OctetString)encap).getOctets()); |
| 228 | + } |
| 229 | + |
| 230 | + // (2) certificates, in wire-encoding order. |
| 231 | + ASN1Set certs = sd.getCertificates(); |
| 232 | + if (certs != null) |
| 233 | + { |
| 234 | + Enumeration e = certs.getObjects(); |
| 235 | + while (e.hasMoreElements()) |
| 236 | + { |
| 237 | + ASN1Encodable c = (ASN1Encodable)e.nextElement(); |
| 238 | + out.write(c.toASN1Primitive().getEncoded(ASN1Encoding.DER)); |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + // (3) CRLs, in wire-encoding order. |
| 243 | + ASN1Set crls = sd.getCRLs(); |
| 244 | + if (crls != null) |
| 245 | + { |
| 246 | + Enumeration e = crls.getObjects(); |
| 247 | + while (e.hasMoreElements()) |
| 248 | + { |
| 249 | + ASN1Encodable c = (ASN1Encodable)e.nextElement(); |
| 250 | + out.write(c.toASN1Primitive().getEncoded(ASN1Encoding.DER)); |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + // (4) Each SignerInfo, with archive-time-stamps stripped from |
| 255 | + // unsignedAttrs and re-DER-encoded. |
| 256 | + for (SignerInformation s : signed.getSignerInfos().getSigners()) |
| 257 | + { |
| 258 | + SignerInformation stripped = stripArchiveTimestamps(s); |
| 259 | + out.write(stripped.toASN1Structure().getEncoded(ASN1Encoding.DER)); |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + private static SignerInformation stripArchiveTimestamps(SignerInformation signer) |
| 264 | + { |
| 265 | + AttributeTable unsigned = signer.getUnsignedAttributes(); |
| 266 | + if (unsigned == null) |
| 267 | + { |
| 268 | + return signer; |
| 269 | + } |
| 270 | + |
| 271 | + boolean hasAny = unsigned.get(id_aa_ets_archiveTimestampV2) != null |
| 272 | + || unsigned.get(PKCSObjectIdentifiers.id_aa_ets_archiveTimestamp) != null; |
| 273 | + if (!hasAny) |
| 274 | + { |
| 275 | + return signer; |
| 276 | + } |
| 277 | + |
| 278 | + ASN1EncodableVector kept = new ASN1EncodableVector(); |
| 279 | + ASN1EncodableVector all = unsigned.toASN1EncodableVector(); |
| 280 | + for (int i = 0; i != all.size(); ++i) |
| 281 | + { |
| 282 | + Attribute a = Attribute.getInstance(all.get(i)); |
| 283 | + ASN1ObjectIdentifier type = a.getAttrType(); |
| 284 | + if (!id_aa_ets_archiveTimestampV2.equals(type) |
| 285 | + && !PKCSObjectIdentifiers.id_aa_ets_archiveTimestamp.equals(type)) |
| 286 | + { |
| 287 | + kept.add(a); |
| 288 | + } |
| 289 | + } |
| 290 | + AttributeTable filtered = kept.size() == 0 ? null : new AttributeTable(kept); |
| 291 | + return SignerInformation.replaceUnsignedAttributes(signer, filtered); |
| 292 | + } |
| 293 | +} |
0 commit comments