BundleReader.java

/*
 * Copyright 2024 The Sigstore Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dev.sigstore.bundle;

import com.google.protobuf.ByteString;
import dev.sigstore.json.ProtoJson;
import dev.sigstore.proto.ProtoMutators;
import dev.sigstore.proto.common.v1.HashAlgorithm;
import dev.sigstore.rekor.client.ImmutableInclusionProof;
import dev.sigstore.rekor.client.ImmutableRekorEntry;
import dev.sigstore.rekor.client.ImmutableVerification;
import java.io.IOException;
import java.io.Reader;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
import org.bouncycastle.util.encoders.Hex;

class BundleReader {

  static Bundle readBundle(Reader jsonReader) throws BundleParseException {
    var protoBundleBuilder = dev.sigstore.proto.bundle.v1.Bundle.newBuilder();
    try {
      ProtoJson.parser().merge(jsonReader, protoBundleBuilder);
    } catch (IOException ioe) {
      throw new BundleParseException("Could not process bundle json", ioe);
    }

    var protoBundle = protoBundleBuilder.build();
    var bundleBuilder = ImmutableBundle.builder();
    if (!Bundle.SUPPORTED_MEDIA_TYPES.contains(protoBundle.getMediaType())) {
      throw new BundleParseException(
          "Unsupported bundle media type: " + protoBundle.getMediaType());
    }

    bundleBuilder.mediaType(protoBundle.getMediaType());

    if (protoBundle.getVerificationMaterial().getTlogEntriesCount() == 0) {
      throw new BundleParseException("Could not find any tlog entries in bundle json");
    }
    for (var bundleEntry : protoBundle.getVerificationMaterial().getTlogEntriesList()) {
      if (!bundleEntry.hasInclusionProof()) {
        // all consumed bundles must have an inclusion proof
        throw new BundleParseException("Could not find an inclusion proof");
      }
      var bundleInclusionProof = bundleEntry.getInclusionProof();

      var inclusionProof =
          ImmutableInclusionProof.builder()
              .logIndex(bundleInclusionProof.getLogIndex())
              .rootHash(Hex.toHexString(bundleInclusionProof.getRootHash().toByteArray()))
              .treeSize(bundleInclusionProof.getTreeSize())
              .checkpoint(bundleInclusionProof.getCheckpoint().getEnvelope())
              .addAllHashes(
                  bundleInclusionProof.getHashesList().stream()
                      .map(ByteString::toByteArray)
                      .map(Hex::toHexString)
                      .collect(Collectors.toList()))
              .build();

      var verification =
          ImmutableVerification.builder()
              .signedEntryTimestamp(
                  Base64.getEncoder()
                      .encodeToString(
                          bundleEntry
                              .getInclusionPromise()
                              .getSignedEntryTimestamp()
                              .toByteArray()))
              .inclusionProof(inclusionProof)
              .build();

      var rekorEntry =
          ImmutableRekorEntry.builder()
              .integratedTime(bundleEntry.getIntegratedTime())
              .logID(Hex.toHexString(bundleEntry.getLogId().getKeyId().toByteArray()))
              .logIndex(bundleEntry.getLogIndex())
              .body(
                  Base64.getEncoder()
                      .encodeToString(bundleEntry.getCanonicalizedBody().toByteArray()))
              .verification(verification)
              .build();

      bundleBuilder.addEntries(rekorEntry);
    }

    if (protoBundle.hasDsseEnvelope()) {
      var dsseEnvelopeProto = protoBundle.getDsseEnvelope();
      var dsseEnvelopeBuilder =
          ImmutableDsseEnvelope.builder()
              .payload(dsseEnvelopeProto.getPayload().toByteArray())
              .payloadType(dsseEnvelopeProto.getPayloadType());
      for (int sigIndex = 0; sigIndex < dsseEnvelopeProto.getSignaturesCount(); sigIndex++) {
        dsseEnvelopeBuilder.addSignatures(
            ImmutableSignature.builder()
                .sig(dsseEnvelopeProto.getSignatures(sigIndex).getSig().toByteArray())
                .build());
      }
      bundleBuilder.dsseEnvelope(dsseEnvelopeBuilder.build());
    } else if (protoBundle.hasMessageSignature()) {
      var signature = protoBundle.getMessageSignature().getSignature().toByteArray();
      if (protoBundle.getMessageSignature().hasMessageDigest()) {
        var hashAlgorithm = protoBundle.getMessageSignature().getMessageDigest().getAlgorithm();
        if (hashAlgorithm != HashAlgorithm.SHA2_256) {
          throw new BundleParseException(
              "Cannot read message digests of type "
                  + hashAlgorithm
                  + ", only "
                  + HashAlgorithm.SHA2_256
                  + " is supported");
        }
        var messageSignature =
            ImmutableMessageSignature.builder()
                .messageDigest(
                    ImmutableMessageDigest.builder()
                        .hashAlgorithm(Bundle.HashAlgorithm.SHA2_256)
                        .digest(
                            protoBundle
                                .getMessageSignature()
                                .getMessageDigest()
                                .getDigest()
                                .toByteArray())
                        .build())
                .signature(signature)
                .build();
        bundleBuilder.messageSignature(messageSignature);
      } else {
        bundleBuilder.messageSignature(
            ImmutableMessageSignature.builder().signature(signature).build());
      }
    } else {
      throw new BundleParseException("A MessageSignature or DSSEEnvelope must be provided");
    }

    CertPath certPath;
    try {
      if (protoBundle.getVerificationMaterial().hasCertificate()) {
        certPath =
            ProtoMutators.toCertPath(
                List.of(protoBundle.getVerificationMaterial().getCertificate()));
      } else if (protoBundle.getVerificationMaterial().hasX509CertificateChain()) {
        certPath =
            ProtoMutators.toCertPath(
                protoBundle
                    .getVerificationMaterial()
                    .getX509CertificateChain()
                    .getCertificatesList());
      } else if (protoBundle.getVerificationMaterial().hasPublicKey()) {
        throw new BundleParseException("Plain public keys are not supported by this client");
      } else {
        throw new BundleParseException("Could not find a certificate or certificate chain");
      }
    } catch (CertificateException ce) {
      throw new BundleParseException("Could not parse bundle certificate chain", ce);
    }
    bundleBuilder.certPath(certPath);

    if (protoBundle.getVerificationMaterial().hasTimestampVerificationData()) {
      for (var timestamp :
          protoBundle
              .getVerificationMaterial()
              .getTimestampVerificationData()
              .getRfc3161TimestampsList()) {
        bundleBuilder.addTimestamps(
            ImmutableTimestamp.builder()
                .rfc3161Timestamp(timestamp.getSignedTimestamp().toByteArray())
                .build());
      }
    }

    return bundleBuilder.build();
  }
}