Skip to content

Commit 5137b9b

Browse files
authored
Cleanup several classes (#772)
Signed-off-by: Valentin Delaye <jonesbusy@users.noreply.github.com>
1 parent b66a78c commit 5137b9b

13 files changed

Lines changed: 362 additions & 622 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,9 @@ made by that key are accepted:
357357
Multiple keys (`keyPaths`/`keyDatas`) and keyless (Fulcio/Rekor) verification are **not** supported.
358358
Signatures are discovered through the OCI [referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers)
359359
(the Sigstore bundle, `application/vnd.dev.sigstore.bundle.v0.3+json`, attached to the image); no
360-
local signature store is consulted. The `signedIdentity` field is accepted but, because the bundle
361-
binds only the image digest, matching is limited to the digest of the pulled image.
360+
local signature store is consulted. Verification binds the signature to the pulled image by its
361+
digest. The `signedIdentity` field is **not supported** and is ignored if present, because the
362+
cosign bundle payload carries only the image digest and no claimed Docker reference to match against.
362363

363364
```json
364365
{

src/main/java/land/oras/ContainerRef.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.regex.Matcher;
2929
import java.util.regex.Pattern;
3030
import land.oras.exception.OrasException;
31+
import land.oras.policy.Transport;
3132
import land.oras.utils.Const;
3233
import land.oras.utils.SupportedAlgorithm;
3334
import org.jspecify.annotations.NullMarked;
@@ -536,7 +537,7 @@ public boolean isBlocked(Registry registry) {
536537
// Check containers policy. Strip a trailing ":tag" and/or "@digest" without touching a
537538
// "host:port" registry (the tag colon always follows the last "/").
538539
String scope = effectiveRef.toString().replaceFirst("(:[^/@]+)?(@[^/]+)?$", "");
539-
boolean allowed = registry.getContainersPolicy().isAllowed("docker", scope);
540+
boolean allowed = registry.getContainersPolicy().isAllowed(Transport.DOCKER, scope);
540541
if (!allowed) {
541542
throw new OrasException("Image '%s' rejected by containers policy".formatted(this));
542543
}

src/main/java/land/oras/Registry.java

Lines changed: 34 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import land.oras.exception.OrasException;
5656
import land.oras.policy.ContainersPolicy;
5757
import land.oras.policy.PolicyContext;
58+
import land.oras.policy.Transport;
5859
import land.oras.utils.ArchiveUtils;
5960
import land.oras.utils.Const;
6061
import land.oras.utils.JsonUtils;
@@ -508,64 +509,6 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType
508509
return JsonUtils.fromJson(response.response(), Referrers.class);
509510
}
510511

511-
/**
512-
* Verify a resolved image against the containers trust policy.
513-
*
514-
* <p>Invoked from the pull path once a manifest/index digest is known. The lightweight scope gate
515-
* ({@link ContainerRef#checkBlocked(Registry)}) has already run; this performs the content-based
516-
* checks (currently Sigstore signature verification for {@code sigstoreSigned} requirements).
517-
*
518-
* @param containerRef the reference being pulled.
519-
* @param digest the resolved manifest/index digest.
520-
* @throws OrasException if the policy rejects the image.
521-
*/
522-
private void verifyContainersPolicy(ContainerRef containerRef, String digest) {
523-
String effectiveRegistry = containerRef.getEffectiveRegistry(this);
524-
ContainerRef effectiveRef = containerRef.forRegistry(effectiveRegistry);
525-
// Strip a trailing ":tag" and/or "@digest" without touching a "host:port" registry (the tag
526-
// colon always follows the last "/").
527-
String scope = effectiveRef.toString().replaceFirst("(:[^/@]+)?(@[^/]+)?$", "");
528-
ContainerRef digestRef = effectiveRef.withDigest(digest);
529-
PolicyContext context = new PolicyContext(
530-
"docker", scope, digest, effectiveRef.toString(), () -> fetchSigstoreBundles(digestRef));
531-
containersPolicy.verify(context);
532-
}
533-
534-
/**
535-
* Fetch the raw bytes of every Sigstore bundle attached to the given image as a referrer.
536-
*
537-
* <p>Discovers referrers whose artifact type is {@link Const#SIGSTORE_BUNDLE_MEDIA_TYPE} and
538-
* returns the bytes of each bundle layer. Uses non-verifying fetches ({@link #getDescriptor} and
539-
* {@link #getBlob}) to avoid recursing back into policy verification. Any failure to enumerate or
540-
* read signatures yields an empty list, which causes a {@code sigstoreSigned} requirement to fail
541-
* closed.
542-
*
543-
* @param digestRef the image reference pinned to its digest.
544-
* @return the bundle blob bytes; empty if none are attached or discovery fails.
545-
*/
546-
private List<byte[]> fetchSigstoreBundles(ContainerRef digestRef) {
547-
List<byte[]> bundles = new ArrayList<>();
548-
try {
549-
Referrers referrers = getReferrers(digestRef, ArtifactType.from(Const.SIGSTORE_BUNDLE_MEDIA_TYPE));
550-
for (ManifestDescriptor referrer : referrers.getManifests()) {
551-
// Some registries ignore the artifactType filter; enforce it client-side too.
552-
if (!Const.SIGSTORE_BUNDLE_MEDIA_TYPE.equals(referrer.getArtifactType())) {
553-
continue;
554-
}
555-
Descriptor descriptor = getDescriptor(digestRef.withDigest(referrer.getDigest()));
556-
Manifest signatureManifest = Manifest.fromJson(descriptor.getJson());
557-
for (Layer layer : signatureManifest.getLayers()) {
558-
if (Const.SIGSTORE_BUNDLE_MEDIA_TYPE.equals(layer.getMediaType())) {
559-
bundles.add(getBlob(digestRef.withDigest(layer.getDigest())));
560-
}
561-
}
562-
}
563-
} catch (Exception e) {
564-
LOG.warn("Failed to fetch Sigstore signatures for {}: {}", digestRef, e.getMessage());
565-
}
566-
return bundles;
567-
}
568-
569512
/**
570513
* Delete a manifest
571514
* @param containerRef The artifact
@@ -1493,6 +1436,39 @@ private void logResponse(HttpClient.ResponseWrapper<?> response) {
14931436
}
14941437
}
14951438

1439+
private void verifyContainersPolicy(ContainerRef containerRef, String digest) {
1440+
String effectiveRegistry = containerRef.getEffectiveRegistry(this);
1441+
ContainerRef effectiveRef = containerRef.forRegistry(effectiveRegistry);
1442+
String scope = effectiveRef.toString().replaceFirst("(:[^/@]+)?(@[^/]+)?$", "");
1443+
ContainerRef digestRef = effectiveRef.withDigest(digest);
1444+
PolicyContext context = new PolicyContext(
1445+
Transport.DOCKER, scope, digest, effectiveRef.toString(), () -> fetchSigstoreBundles(digestRef));
1446+
containersPolicy.verify(context);
1447+
}
1448+
1449+
private List<byte[]> fetchSigstoreBundles(ContainerRef digestRef) {
1450+
List<byte[]> bundles = new ArrayList<>();
1451+
try {
1452+
Referrers referrers = getReferrers(digestRef, ArtifactType.from(Const.SIGSTORE_BUNDLE_MEDIA_TYPE));
1453+
for (ManifestDescriptor referrer : referrers.getManifests()) {
1454+
// Some registries ignore the artifactType filter
1455+
if (!Const.SIGSTORE_BUNDLE_MEDIA_TYPE.equals(referrer.getArtifactType())) {
1456+
continue;
1457+
}
1458+
Descriptor descriptor = getDescriptor(digestRef.withDigest(referrer.getDigest()));
1459+
Manifest signatureManifest = Manifest.fromJson(descriptor.getJson());
1460+
for (Layer layer : signatureManifest.getLayers()) {
1461+
if (Const.SIGSTORE_BUNDLE_MEDIA_TYPE.equals(layer.getMediaType())) {
1462+
bundles.add(getBlob(digestRef.withDigest(layer.getDigest())));
1463+
}
1464+
}
1465+
}
1466+
} catch (Exception e) {
1467+
LOG.warn("Failed to fetch Sigstore signatures for {}: {}", digestRef, e.getMessage());
1468+
}
1469+
return bundles;
1470+
}
1471+
14961472
/**
14971473
* Get blob as stream to avoid loading into memory
14981474
* @param containerRef The container ref

src/main/java/land/oras/policy/ContainersPolicy.java

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.nio.file.Files;
2626
import java.nio.file.Path;
2727
import java.util.Collections;
28+
import java.util.LinkedHashMap;
2829
import java.util.List;
2930
import java.util.Map;
3031
import land.oras.OrasModel;
@@ -47,16 +48,15 @@
4748
* verification is required.
4849
*
4950
* @see PolicyRequirement
50-
* @see SignedIdentity
5151
*/
5252
@NullMarked
5353
public class ContainersPolicy {
5454

5555
private static final Logger LOG = LoggerFactory.getLogger(ContainersPolicy.class);
5656

5757
/**
58-
* A dedicated Jackson mapper for policy.json that supports {@link PolicyRequirement} and
59-
* {@link SignedIdentity} polymorphic deserialization.
58+
* A dedicated Jackson mapper for policy.json that supports {@link PolicyRequirement}
59+
* polymorphic deserialization.
6060
*
6161
* <p>The global mapper in {@link JsonUtils} has a {@code NON_EMPTY} global inclusion filter
6262
* that would interfere with the {@code @JsonTypeInfo} resolution here, so we use a separate
@@ -148,26 +148,26 @@ public static ContainersPolicy rejectAll() {
148148
* operation to proceed here; their cryptographic check runs in {@link #verify(PolicyContext)} once
149149
* the image has been resolved during a pull.
150150
*
151-
* @param transport the transport name, e.g. {@code "docker"}.
151+
* @param transport the transport, e.g. {@link Transport#DOCKER}.
152152
* @param scope the image scope, e.g. {@code "docker.io/library/nginx"}.
153153
* @return {@code true} if all resolved requirements pass.
154154
*/
155-
public boolean isAllowed(String transport, String scope) {
155+
public boolean isAllowed(Transport transport, String scope) {
156156
PolicyContext context = PolicyContext.forScope(transport, scope);
157157
List<PolicyRequirement> requirements = resolveRequirements(transport, scope);
158158
for (PolicyRequirement req : requirements) {
159159
if (!req.verify(context)) {
160-
LOG.debug("Policy requirement {} failed for transport='{}' scope='{}'", req, transport, scope);
160+
LOG.debug("Policy requirement {} failed for transport {} and scope {}", req, transport, scope);
161161
return false;
162162
}
163163
}
164-
LOG.debug("Policy all requirements passed for transport='{}' scope='{}'", transport, scope);
164+
LOG.debug("Policy all requirements passed for transport {} and scope {}", transport, scope);
165165
return true;
166166
}
167167

168168
/**
169169
* Verify a resolved image against this policy, performing content-based checks (such as Sigstore
170-
* signature verification) that {@link #isAllowed(String, String)} cannot perform.
170+
* signature verification) that {@link #isAllowed(Transport, String)} cannot perform.
171171
*
172172
* <p>All resolved requirements must pass (logical AND). If any requirement fails, an
173173
* {@link OrasException} is thrown describing the failure.
@@ -190,18 +190,18 @@ public void verify(PolicyContext context) {
190190
* Resolve the list of {@link PolicyRequirement} objects that apply to the given transport and
191191
* scope, following the precedence rules described in {@link #isAllowed}.
192192
*
193-
* @param transport the transport name, e.g. {@code "docker"}.
193+
* @param transport the transport, e.g. {@link Transport#DOCKER}.
194194
* @param scope the image scope, e.g. {@code "docker.io/library/nginx"}.
195195
* @return the non-null, possibly empty list of requirements (empty means global default
196196
* was used and it too was empty — treat as reject-by-default for safety).
197197
*/
198-
public List<PolicyRequirement> resolveRequirements(String transport, String scope) {
198+
public List<PolicyRequirement> resolveRequirements(Transport transport, String scope) {
199199
Map<String, List<PolicyRequirement>> transportMap =
200200
policyFile.transports().getOrDefault(transport, Collections.emptyMap());
201201

202202
// Exact match
203203
if (transportMap.containsKey(scope)) {
204-
LOG.debug("Policy: exact match for transport='{}' scope='{}'", transport, scope);
204+
LOG.debug("Policy: exact match for transport {} and scope {}", transport, scope);
205205
return transportMap.get(scope);
206206
}
207207

@@ -216,7 +216,7 @@ public List<PolicyRequirement> resolveRequirements(String transport, String scop
216216
}
217217
}
218218
if (best != null) {
219-
LOG.debug("Policy: prefix match '{}' for transport='{}' scope='{}'", best, transport, scope);
219+
LOG.debug("Policy: prefix match '{}' for transport {} and scope {}", best, transport, scope);
220220
return transportMap.get(best);
221221
}
222222

@@ -230,18 +230,18 @@ public List<PolicyRequirement> resolveRequirements(String transport, String scop
230230
}
231231
}
232232
if (bestWildcard != null) {
233-
LOG.debug("Policy: wildcard match '{}' for transport='{}' scope='{}'", bestWildcard, transport, scope);
233+
LOG.debug("Policy: wildcard match '{}' for transport {} and scope {}", bestWildcard, transport, scope);
234234
return transportMap.get(bestWildcard);
235235
}
236236

237237
// Transport default
238238
if (transportMap.containsKey("")) {
239-
LOG.debug("Policy: transport default for transport='{}'", transport);
239+
LOG.debug("Policy: transport default for transport {}", transport);
240240
return transportMap.get("");
241241
}
242242

243243
// Default
244-
LOG.debug("Policy: global default for transport='{}' scope='{}'", transport, scope);
244+
LOG.debug("Policy: global default for transport {} and scope {}", transport, scope);
245245
return policyFile.defaultRequirements();
246246
}
247247

@@ -257,9 +257,9 @@ public List<PolicyRequirement> getDefaultRequirements() {
257257
/**
258258
* Return all transport-scoped requirements as an unmodifiable map.
259259
*
260-
* @return a map from transport name to a map of scope → requirements.
260+
* @return a map from {@link Transport} to a map of scope → requirements.
261261
*/
262-
public Map<String, Map<String, List<PolicyRequirement>>> getTransports() {
262+
public Map<Transport, Map<String, List<PolicyRequirement>>> getTransports() {
263263
return Collections.unmodifiableMap(policyFile.transports());
264264
}
265265

@@ -318,21 +318,30 @@ private static List<Path> defaultPolicyPaths() {
318318
*/
319319
@OrasModel
320320
record PolicyFile(
321-
@JsonProperty("default") List<PolicyRequirement> defaultRequirements,
322-
@JsonProperty("transports") Map<String, Map<String, List<PolicyRequirement>>> transports) {
321+
List<PolicyRequirement> defaultRequirements,
322+
Map<Transport, Map<String, List<PolicyRequirement>>> transports) {
323323

324324
/**
325-
* Creates a new {@link PolicyFile}.
325+
* Deserialize a {@link PolicyFile} from its JSON form, mapping the raw transport keys to the
326+
* {@link Transport} enum (any non-{@code docker} transport is merged into {@link Transport#UNKNOWN}).
326327
*
327-
* @param defaultRequirements the global default requirements.
328-
* @param transports the per-transport requirements.
328+
* @param defaultRequirements the global default requirements (key {@code "default"}).
329+
* @param rawTransports the per-transport requirements keyed by raw transport name.
330+
* @return the parsed policy file.
329331
*/
330332
@JsonCreator
331-
PolicyFile(
333+
static PolicyFile fromJson(
332334
@JsonProperty("default") @Nullable List<PolicyRequirement> defaultRequirements,
333-
@JsonProperty("transports") @Nullable Map<String, Map<String, List<PolicyRequirement>>> transports) {
334-
this.defaultRequirements = defaultRequirements != null ? defaultRequirements : Collections.emptyList();
335-
this.transports = transports != null ? transports : Collections.emptyMap();
335+
@JsonProperty("transports") @Nullable Map<String, Map<String, List<PolicyRequirement>>> rawTransports) {
336+
List<PolicyRequirement> defaults =
337+
defaultRequirements != null ? defaultRequirements : Collections.emptyList();
338+
Map<Transport, Map<String, List<PolicyRequirement>>> byTransport = new LinkedHashMap<>();
339+
if (rawTransports != null) {
340+
rawTransports.forEach((name, scopes) -> byTransport
341+
.computeIfAbsent(Transport.fromValue(name), t -> new LinkedHashMap<>())
342+
.putAll(scopes));
343+
}
344+
return new PolicyFile(defaults, byTransport);
336345
}
337346
}
338347
}

0 commit comments

Comments
 (0)