BundleVerifier.java

/*
 * Copyright 2023 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.api.FieldBehavior;
import com.google.api.FieldBehaviorProto;
import com.google.protobuf.Descriptors;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.util.JsonFormat;
import dev.sigstore.json.ProtoJson;
import dev.sigstore.proto.bundle.v1.Bundle;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/** Implements Sigstore Bundle verification. */
public class BundleVerifier {
  static final JsonFormat.Parser JSON_PARSER = ProtoJson.parser();

  /**
   * Parses Sigstore Bundle JSON into protobuf.
   *
   * @param bundleJson input JSON
   * @return a list of required fields missing in a bundle
   * @throws IllegalArgumentException if the JSON is invalid
   */
  public static List<String> allMissingFields(String bundleJson) {
    var bundle = Bundle.newBuilder();
    try {
      JSON_PARSER.merge(bundleJson, bundle);
    } catch (InvalidProtocolBufferException e) {
      throw new IllegalArgumentException("Unable to parse Sigstore Bundle " + bundleJson, e);
    }
    return findMissingFields(bundle);
  }

  static List<String> findMissingFields(MessageOrBuilder message) {
    final List<String> missing = new ArrayList<>();
    findMissingFields(message, "", missing);
    return missing;
  }

  private static void findMissingFields(
      MessageOrBuilder message, String prefix, List<String> missing) {
    // Get missing fields of the message itself
    for (Descriptors.FieldDescriptor field : message.getDescriptorForType().getFields()) {
      if (!isRequired(field)) {
        continue;
      }
      // The parts of a "oneof {...}" are not required on their own
      if (field.getContainingOneof() != null) {
        continue;
      }
      // The field was required, so verify if it contains data
      if (field.isRepeated()) {
        if (message.getRepeatedFieldCount(field) != 0) {
          // Repeated field has values => OK
          continue;
        }
      } else if (message.hasField(field)) {
        // Field is present => OK
        continue;
      }
      // Field is missing
      missing.add(prefix + field.getName());
    }
    // Verify oneof fields. They are required if any of the fields in the oneof is required.
    for (Descriptors.OneofDescriptor oneof : message.getDescriptorForType().getRealOneofs()) {
      if (!message.hasOneof(oneof)
          && oneof.getFields().stream().anyMatch(BundleVerifier::isRequired)) {
        missing.add(prefix + oneof.getName());
      }
    }
    // Recurse into each set message field
    // getAllFields returns only present fields
    for (final Map.Entry<Descriptors.FieldDescriptor, Object> entry :
        message.getAllFields().entrySet()) {
      Descriptors.FieldDescriptor field = entry.getKey();
      Object value = entry.getValue();

      if (field.getJavaType() != Descriptors.FieldDescriptor.JavaType.MESSAGE) {
        continue;
      }
      if (!field.isRepeated()) {
        findMissingFields((MessageOrBuilder) value, subMessagePrefix(prefix, field, -1), missing);
      } else {
        int i = 0;
        //noinspection unchecked
        for (final MessageOrBuilder element : (List<MessageOrBuilder>) value) {
          findMissingFields(element, subMessagePrefix(prefix, field, i++), missing);
        }
      }
    }
  }

  private static String subMessagePrefix(
      String prefix, Descriptors.FieldDescriptor field, int index) {
    StringBuilder result = new StringBuilder(prefix);
    if (field.isExtension()) {
      result.append('(').append(field.getFullName()).append(')');
    } else {
      result.append(field.getName());
    }
    if (index != -1) {
      result.append('[').append(index).append(']');
    }
    result.append('.');
    return result.toString();
  }

  static boolean isRequired(Descriptors.FieldDescriptor field) {
    return field.isRequired()
        || field
            .toProto()
            .getOptions()
            .getExtension(FieldBehaviorProto.fieldBehavior)
            .contains(FieldBehavior.REQUIRED);
  }
}