Bundle.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.common.base.Preconditions;
import dev.sigstore.rekor.client.RekorEntry;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertPath;
import java.util.List;
import java.util.Optional;
import org.immutables.gson.Gson;
import org.immutables.value.Value;
import org.immutables.value.Value.Default;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;
import org.immutables.value.Value.Lazy;

/**
 * A representation of sigstore signing materials. See <a
 * href="https://github.com/sigstore/protobuf-specs">protobuf-specs</a>
 */
@Immutable
public abstract class Bundle {

  public enum HashAlgorithm {
    SHA2_256
  }

  static final String BUNDLE_V_0_1 = "application/vnd.dev.sigstore.bundle+json;version=0.1";
  static final String BUNDLE_V_0_2 = "application/vnd.dev.sigstore.bundle+json;version=0.2";
  static final String BUNDLE_V_0_3 = "application/vnd.dev.sigstore.bundle+json;version=0.3";
  // media_type format switch: https://github.com/sigstore/protobuf-specs/pull/279
  static final String BUNDLE_V_0_3_1 = "application/vnd.dev.sigstore.bundle.v0.3+json";
  static final List<String> SUPPORTED_MEDIA_TYPES =
      List.of(BUNDLE_V_0_1, BUNDLE_V_0_2, BUNDLE_V_0_3, BUNDLE_V_0_3_1);

  /** The bundle version */
  @Default
  public String getMediaType() {
    return BUNDLE_V_0_3_1;
  }

  /** A signature represented as a signature and digest */
  public abstract Optional<MessageSignature> getMessageSignature();

  /** A DSSE envelope signature type that may contain an arbitrary payload */
  public abstract Optional<DsseEnvelope> getDsseEnvelope();

  @Value.Check
  protected void checkOnlyOneSignature() {
    Preconditions.checkState(
        (getDsseEnvelope().isEmpty() && getMessageSignature().isPresent())
            || (getDsseEnvelope().isPresent() && getMessageSignature().isEmpty()));
  }

  @Value.Check
  protected void checkAtLeastOneTimestamp() {
    for (var entry : getEntries()) {
      if (entry.getVerification().getSignedEntryTimestamp() != null) {
        return;
      }
    }
    if (getTimestamps().size() > 0) {
      return;
    }
    throw new IllegalStateException("No timestamp verification (set, timestamp) was provided");
  }

  /**
   * The partial certificate chain provided by fulcio for the public key and identity used to sign
   * the artifact, this should NOT contain the trusted root or any trusted intermediates. But users
   * of this object should understand that older signatures may include the full chain.
   */
  public abstract CertPath getCertPath();

  /**
   * The entry in the rekor transparency log (represented as a list for future compatibility, but
   * currently only allow for at most one entry.
   */
  public abstract List<RekorEntry> getEntries();

  /** A list of timestamps to verify the time of signing. Currently, allows rfc3161 timestamps. */
  public abstract List<Timestamp> getTimestamps();

  @Immutable
  public interface MessageSignature {

    /**
     * An optional message digest, this should not be used to verify signature validity. A digest
     * should be provided or computed by the system.
     */
    Optional<MessageDigest> getMessageDigest();

    /** Signature over an artifact. */
    byte[] getSignature();

    static MessageSignature of(HashAlgorithm algorithm, byte[] digest, byte[] signature) {
      return ImmutableMessageSignature.builder()
          .signature(signature)
          .messageDigest(
              ImmutableMessageDigest.builder().digest(digest).hashAlgorithm(algorithm).build())
          .build();
    }
  }

  @Immutable
  public interface MessageDigest {

    /** The algorithm used to compute the digest. */
    HashAlgorithm getHashAlgorithm();

    /**
     * The raw bytes of the digest computer using the hashing algorithm described by {@link
     * #getHashAlgorithm()}
     */
    byte[] getDigest();
  }

  @Immutable
  public interface DsseEnvelope {

    /** An arbitrary payload that does not need to be parsed to be validated */
    byte[] getPayload();

    /** Information on how to interpret the payload */
    String getPayloadType();

    /** DSSE specific signature */
    List<Signature> getSignatures();

    /**
     * The "Pre-Authentication Encoding" of this statement. The signature is generated over this
     * content.
     */
    @Gson.Ignore
    @Derived
    default byte[] getPAE() {
      return ("DSSEv1 "
              + getPayloadType().length()
              + " "
              + getPayloadType()
              + " "
              + getPayloadAsString().length()
              + " "
              + getPayloadAsString())
          .getBytes(StandardCharsets.UTF_8);
    }

    @Lazy
    @Gson.Ignore
    default String getPayloadAsString() {
      return new String(getPayload(), StandardCharsets.UTF_8);
    }

    @Lazy
    @Gson.Ignore
    default byte[] getSignature() {
      return getSignatures().get(0).getSig();
    }

    @Immutable
    interface Signature {
      byte[] getSig();
    }
  }

  @Immutable
  public interface Timestamp {

    /** Raw bytes of an rfc3161 timestamp */
    byte[] getRfc3161Timestamp();
  }

  /** Read a json formatted bundle. */
  public static Bundle from(Reader bundleJson) throws BundleParseException {
    return BundleReader.readBundle(bundleJson);
  }

  /** Read a json formatted bundle from a file. */
  public static Bundle from(Path file, Charset cs) throws BundleParseException, IOException {
    return BundleReader.readBundle(Files.newBufferedReader(file, cs));
  }

  @Lazy
  public String toJson() {
    return BundleWriter.writeBundle(this);
  }
}