Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERSEvidenceRecord may read InputStream multiple times #1859

Open
takaml opened this issue Oct 4, 2024 · 0 comments
Open

ERSEvidenceRecord may read InputStream multiple times #1859

takaml opened this issue Oct 4, 2024 · 0 comments
Assignees

Comments

@takaml
Copy link

takaml commented Oct 4, 2024

I'm currently using version 1.78.1.

The request returned by ERSEvidenceRecord.generateHashRenewalRequest() appears to contain an incorrect message digest if the data is provided as an ERSInputStreamData. Below is some code to reproduce the issue.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;

import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampRequest;
import org.bouncycastle.tsp.TimeStampRequestGenerator;
import org.bouncycastle.tsp.ers.ERSByteData;
import org.bouncycastle.tsp.ers.ERSData;
import org.bouncycastle.tsp.ers.ERSEvidenceRecord;
import org.bouncycastle.tsp.ers.ERSException;
import org.bouncycastle.tsp.ers.ERSInputStreamData;

public class ERTest
{
    public void erTest () throws TSPException, ERSException, OperatorCreationException, IOException,
            NoSuchAlgorithmException
    {
        // ER for the String "foo" using SHA-256 and a dummy cert/key.
        String evidenceRecordBase64 = "MIIDCgIBATANMAsGCWCGSAFlAwQCATCCAvQwggLwMIIC7DCCAugGCSqGSI"
                + "b3DQEHAqCCAtkwggLVAgEDMQ0wCwYJYIZIAWUDBAIDMG0GCyqGSIb3DQEJEAEEoF4EXDBaAgEBBgYEA"
                + "I9nAQEwLzALBglghkgBZQMEAgEEICwmtGto/8aP+ZtFPB0wQTQTQi1wZIO/oPmKXohiZueuAgEBGA8y"
                + "MDI0MTAwMjIzMDAzNloCCFO56J9TFmGGMYICUDCCAkwCAQEwBTAAAgEAMAsGCWCGSAFlAwQCA6CCAR4"
                + "wGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMDIyMzAwMzZaMC"
                + "sGCSqGSIb3DQEJNDEeMBwwCwYJYIZIAWUDBAIDoQ0GCSqGSIb3DQEBDQUAME8GCSqGSIb3DQEJBDFCB"
                + "EDPPkk9PN5EGrAvZyjv9wJR77tQCIwoA3xWwqESBGICShZxNjt6YU3LZor99ARJ4As+yiIjW/hTg5v3"
                + "vqbTrfSIMGQGCyqGSIb3DQEJEAIvMVUwUzBRME8wCwYJYIZIAWUDBAIDBEAPvi2mNblzqqI91nWRM9s"
                + "ocS7TJfpyQsx4ZcBVeGK1XjCW6BQ5KmrPFCc+IefB5FB/ZQsPwdyYv6umJzCYK0SzMA0GCSqGSIb3DQ"
                + "EBDQUABIIBALWgWcjxzY5QEOlK92GNf9kjBflbO65dYkAKxrrgcwQ6Dz+ablUwsG01ILDUUnSL9wTQC"
                + "OkYKb1oEFNrd9lbHWBOqlu5/lMjhZcWnYzbK3rzQRuoPwXYD/GWgiO0wLmF3FQ9xaum1Oui+Y075OS4"
                + "7fXfLlSe2wMPlnoDb/IFAgHGBK/3zJ7w7n9OCa1U6qwTYCpw9MTXsOI/PbNw2h3cHTVgbY+HCTB4oJC"
                + "GpY9bbEMuJboe4DkQx2Eqpq1pVaMKRxsjhrnbH8QlkUGtuGztqnZa5AoCth79x70Ch7WhdDcxG3wiFi"
                + "29pw69obUCh3c61Q2WKl+MKW/tqq7EGYu5+jE=";
        DigestCalculatorProvider digestProvider = new BcDigestCalculatorProvider();
        ERSEvidenceRecord ersEvidenceRecord = new ERSEvidenceRecord(
                Base64.getDecoder().decode(evidenceRecordBase64), digestProvider);

        // Sanity check, make sure root hash of ER is what we expect.
        byte[] sourceData = "foo".getBytes(StandardCharsets.UTF_8);
        byte[] sourceSha256 = MessageDigest.getInstance("SHA-256").digest(sourceData);
        assert Arrays.equals(sourceSha256, ersEvidenceRecord.getPrimaryRootHash());


        // Generate hash renewal request using ERSInputStreamData.
        ERSData ersStreamData = new ERSInputStreamData(new ByteArrayInputStream(sourceData));
        TimeStampRequest streamDataReq = ersEvidenceRecord.generateHashRenewalRequest(
                digestProvider.get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha512)),
                ersStreamData,
                new TimeStampRequestGenerator(),
                BigInteger.ZERO);


        // Generate hash renewal request using ERSByteData to compare against.
        ERSData ersByteData = new ERSByteData(sourceData);
        TimeStampRequest byteDataReq = ersEvidenceRecord.generateHashRenewalRequest(
                digestProvider.get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha512)),
                ersByteData,
                new TimeStampRequestGenerator(),
                BigInteger.ZERO);


        // Fails, should pass.
        assert Arrays.equals(byteDataReq.getMessageImprintDigest(),
                streamDataReq.getMessageImprintDigest());


        // Generate the digest we expect to see in the requests and compare.
        byte[] expectedDigest = generateExpectedRequestDigest(sourceData, ersEvidenceRecord,
                MessageDigest.getInstance("SHA-512"));
        assert Arrays.equals(byteDataReq.getMessageImprintDigest(), expectedDigest);
        // Fails, should pass.
        assert Arrays.equals(streamDataReq.getMessageImprintDigest(), expectedDigest);
    }

    /** Based on RFC 4998 section 5.2. */
    private static byte[] generateExpectedRequestDigest (byte[] sourceData,
            ERSEvidenceRecord evidenceRecord, MessageDigest digest) throws IOException
    {
        byte[] atsci = evidenceRecord.toASN1Structure().getArchiveTimeStampSequence()
                .getEncoded(ASN1Encoding.DER);
        byte[] hi = digest.digest(sourceData);
        byte[] hai = digest.digest(atsci);
        byte[] hihai;
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            outputStream.write(hi);
            outputStream.write(hai);
            hihai = outputStream.toByteArray();
        }
        byte[] hiprime = digest.digest(hihai);
        return hiprime;
    }
}

It looks like what's happening is that ERSEvidenceRecord.generateHashRenewalRequest() will first call ERSArchiveTimeStamp.validatePresent(), which will read the stream and generate a digest using the current algorithm (SHA-256 in this example). It later needs to generate the digest a second time using the new algorithm (SHA-512), but at that point the stream has already been consumed. In other places a cached digest would be returned, but that isn't possible in the case of hash renewal since two different digests are required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants