Service.java
/*
* Copyright 2025 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.trustroot;
import dev.sigstore.proto.trustroot.v1.ServiceConfiguration;
import java.net.URI;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import org.immutables.value.Value.Immutable;
@Immutable
public interface Service {
/** The URL of the remote service infrastructure piece. */
URI getUrl();
/** The API version of the service. */
int getApiVersion();
/** The validity window in which this service may be used for signing. */
ValidFor getValidFor();
static Service from(dev.sigstore.proto.trustroot.v1.Service service) {
return ImmutableService.builder()
.apiVersion(service.getMajorApiVersion())
.validFor(ValidFor.from(service.getValidFor()))
.url(URI.create(service.getUrl()))
.build();
}
/**
* INTERNAL ONLY: Returns a default Service object for a url that is valid forever. Only used
* transitionally as we adopt SigningConfig throughout the ecosystem.
*/
static Service of(URI url, int apiVersion) {
return ImmutableService.builder()
.apiVersion(apiVersion)
.validFor(ImmutableValidFor.builder().start(Instant.now()).build())
.url(url)
.build();
}
/**
* Return a single service that is currently valid, that also exposes an api version supported by
* this client. If multiple services match that criteria, filter by those services with the
* highest apiVersion and further sort by services that were started most recently.
*
* @param services the service list
* @param apiVersions an list of api version this clients supports
* @return A service if found
*/
static Optional<Service> select(List<Service> services, List<Integer> apiVersions) {
OptionalInt maxApiVersionMaybe =
services.stream().mapToInt(Service::getApiVersion).filter(apiVersions::contains).max();
if (maxApiVersionMaybe.isEmpty()) {
return Optional.empty();
}
int maxApiVersion = maxApiVersionMaybe.getAsInt();
return services.stream()
.filter(s -> s.getValidFor().contains(Instant.now()))
.filter(s -> s.getApiVersion() == maxApiVersion)
.max(Comparator.comparingLong(s -> s.getValidFor().getStart().toEpochMilli()));
}
@Immutable
interface Config {
enum Selector {
ANY,
EXACT,
ALL
}
// the number to select when selector is EXACT
OptionalInt getCount();
// the selector type
Selector getSelector();
static Config from(ServiceConfiguration config) throws SigstoreConfigurationException {
switch (config.getSelector()) {
case ANY:
return ImmutableConfig.builder().selector(Selector.ANY).build();
case EXACT:
return ImmutableConfig.builder()
.selector(Selector.EXACT)
.count(config.getCount())
.build();
case ALL:
return ImmutableConfig.builder().selector(Selector.ALL).build();
default:
throw new SigstoreConfigurationException(
"Cannot parse signing configuration selector: " + config.getSelector());
}
}
/**
* INTERNAL ONLY: Returns the default config of ANY, only used transitionally as we adopt
* SigningConfig throughout the ecosystem.
*/
static Config ofAny() {
return ImmutableConfig.builder().selector(Selector.ANY).build();
}
}
}