Warning
The Oras Java SDK is currently in alpha state.
It's configuration and APIs might change in future releases
OCI Registry as Storage enables libraries to push OCI Artifacts to OCI Conformant registries. This is a Java SDK for Java developers to empower them to do this in their applications.
SNAPSHOT for version 0.2.x are published on GitHub Maven packages. SNAPSHOT for version 0.3.x and above are published on Maven Central at: https://central.sonatype.com/repository/maven-snapshots/
Releases are published on Maven Central since version 0.2.x.
Javadoc is published from main branch into: https://oras-project.github.io/oras-java/
<dependency>
<groupId>land.oras</groupId>
<artifactId>oras-java-sdk</artifactId>
<version>VERSION_HERE</version>
</dependency>Quarkus users can use the extension quarkus-oras to use the SDK in their applications.
Follow the Quarkus ORAS documentation to get started with Quarkus.
Then on your pom.xml
<repositories>
<repository>
<id>central-snapshots</id>
<name>ORAS Maven Central SNAPSHOTS</name>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>Using existing credentials from ~/.docker/config.json or $XDG_RUNTIME_DIR/containers/auth.json:
Registryregistry = Registry.builder().defaults().build();Using a username and password:
Registryregistry = Registry.builder().defaults("username", "password").build();Registryregistry = Registry.builder().insecure().build();
LocalPathartifact = LocalPath.of(Path.of("my-file.txt"));
Manifestmanifest = registry.pushArtifact(ContainerRef.parse("localhost:5000/hello:v1"), artifact);Push several files at once with a custom artifact type, per-file media types, and manifest-level annotations:
Registryregistry = Registry.builder().insecure().build();
Annotationsannotations = Annotations.ofManifest(Map.of("build-tool", "maven"))
.withFileAnnotations("pom.xml", Map.of("format", "xml"));
Manifestmanifest = registry.pushArtifact(
ContainerRef.parse("localhost:5000/my-app:v1"),
ArtifactType.from("application/vnd.maven+type"),
annotations,
LocalPath.of(Path.of("pom.xml"), "application/xml"),
LocalPath.of(Path.of("target/app.jar"), "application/java-archive"));The resulting manifest will contain one layer per file, each annotated with its filename via org.opencontainers.image.title.
Directories are automatically compressed as a tar+gzip archive and tagged with
org.opencontainers.image.title set to the directory name. The io.deis.oras.content.unpack
annotation is set to true so the SDK automatically extracts the archive on pull.
Registryregistry = Registry.builder().insecure().build();
Manifestmanifest = registry.pushArtifact(
ContainerRef.parse("localhost:5000/my-configs:v1"),
LocalPath.of(Path.of("config-dir")));To push a directory as a plain zip instead:
Manifestmanifest = registry.pushArtifact(
ContainerRef.parse("localhost:5000/my-configs:v1"),
LocalPath.of(Path.of("config-dir"), "application/zip"));Files are automatically written using the org.opencontainers.image.title layer annotation as the filename.
The third argument controls whether existing files are overwritten:
Registryregistry = Registry.builder().insecure().build();
registry.pullArtifact(ContainerRef.parse("localhost:5000/hello:v1"), Path.of("output-dir"), true);Attach a signature or attestation to an already-pushed artifact. The attached manifest references the
original via its subject field and is discoverable through the Referrers API:
Registryregistry = Registry.builder().insecure().build();
ContainerRefref = ContainerRef.parse("localhost:5000/my-app:v1");
// Push the main artifact firstManifestmanifest = registry.pushArtifact(ref,
ArtifactType.from("application/vnd.maven+type"),
LocalPath.of(Path.of("pom.xml"), "application/xml"));
// Attach a signature as a referrerManifestsignatureManifest = registry.attachArtifact(
ref,
ArtifactType.from("application/vnd.example.signature"),
LocalPath.of(Path.of("pom.xml.asc")));
// List all referrers for the artifactReferrersreferrers = registry.getReferrers(
ref.withDigest(manifest.getDescriptor().getDigest()), null);For fine-grained control, push blobs and configs individually before assembling and pushing the manifest:
Registryregistry = Registry.builder().insecure().build();
ContainerRefref = ContainerRef.parse("localhost:5000/my-app:v1");
// Push individual layersLayerlayer1 = registry.pushBlob(ref, Files.readAllBytes(Path.of("schema.json")))
.withAnnotations(Map.of(Const.ANNOTATION_TITLE, "schema.json"));
Layerlayer2 = registry.pushBlob(ref, Files.readAllBytes(Path.of("data.csv")))
.withAnnotations(Map.of(Const.ANNOTATION_TITLE, "data.csv"));
// Push a custom configConfigconfig = registry.pushConfig(ref, Config.empty().withMediaType("application/vnd.example.config+json"));
// Assemble and push the manifestManifestmanifest = Manifest.empty()
.withConfig(config)
.withLayers(List.of(layer1, layer2));
registry.pushManifest(ref, manifest);Copy a tagged artifact — including all its blobs — from one registry to another:
Registrysource = Registry.builder().defaults("user", "pass").insecure().build();
Registrytarget = Registry.builder().defaults("user", "pass").build();
ContainerReffrom = ContainerRef.parse("localhost:5000/my-app:v1");
ContainerRefto = ContainerRef.parse("registry.example.com/my-app:v1");
CopyUtils.copy(source, from, target, to, CopyUtils.CopyOptions.shallow());OCI Layout lets you work with artifacts stored on disk in the OCI Image Layout format.
Push to an OCI Layout directory:
LayoutRefref = LayoutRef.parse("/tmp/my-layout:latest");
OCILayoutociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout")).build();
Manifestmanifest = ociLayout.pushArtifact(
ref,
ArtifactType.from("application/vnd.example.type"),
Annotations.empty(),
LocalPath.of(Path.of("my-file.txt"), "text/plain"));Pull from an OCI Layout directory:
LayoutRefref = LayoutRef.parse("/tmp/my-layout:latest");
OCILayoutociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout")).build();
ociLayout.pullArtifact(ref, Path.of("output-dir"), false);Tar-backed OCI Layout (single-file, portable archive):
LayoutRefref = LayoutRef.parse("/tmp/my-layout.tar:latest");
OCILayoutociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout.tar")).build();
ociLayout.pushArtifact(ref, ArtifactType.from("application/vnd.example.type"),
Annotations.empty(), LocalPath.of(Path.of("my-file.txt"), "text/plain"));
// Pull from the same tarociLayout.pullArtifact(ref, Path.of("output-dir"), false);Copy from OCI Layout to a registry:
LayoutReflayoutRef = LayoutRef.parse("/tmp/my-layout:latest");
OCILayoutociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout")).build();
Registryregistry = Registry.builder().defaults("user", "pass").build();
ContainerReftarget = ContainerRef.parse("registry.example.com/my-app:v1");
CopyUtils.copy(ociLayout, layoutRef, registry, target, CopyUtils.CopyOptions.shallow());Since version 0.7.0 the ORAS Java SDK supports the registries.conf format
(see the containers/image documentation).
The SDK reads configuration from the following locations, in order (later entries override earlier ones):
/etc/containers/registries.conf/etc/containers/registries.conf.d/*.conf(alphabetical)$HOME/.config/containers/registries.conf$HOME/.config/containers/registries.conf.d/*.conf(alphabetical)
# Short-name resolution mode (enforcing is the default)short-name-mode = "enforcing"unqualified-search-registries = ["docker.io"]
# Rewrite a location via a prefix
[[registry]]
prefix = "docker.io/bitnami"location = "docker.io/bitnamilegacy"# Block a registry
[[registry]]
prefix = "gcr.io"blocked = true# Mark a registry as insecure
[[registry]]
location = "localhost:5000"insecure = true# Mirrors — tried in order before falling back to the upstream registry
[[registry]]
prefix = "registry.example.com"location = "registry.example.com"mirror-by-digest-only = false# set to true to restrict all mirrors to digest-only pulls
[[registry.mirror]]
location = "mirror1.example.com"insecure = falsepull-from-mirror = "all"# "all" (default) | "tag-only" | "digest-only"
[[registry.mirror]]
location = "mirror2.example.com"insecure = truepull-from-mirror = "digest-only"Reference resolution always happens before any security decision, and every security decision is evaluated against the effective (resolved) reference — not the reference originally supplied. For each operation the SDK:
- Resolves the reference: short-name / unqualified-search expansion (e.g.
nginx→docker.io/library/nginx),prefix→locationrewrites, and mirror selection. - Evaluates
blocked,insecure(HTTP vs HTTPS) and the trust policy against that resolved reference. - Connects and transfers bytes.
This ordering is intentional: block-list and plaintext decisions must bind to the
host the request actually reaches. Evaluating them on the pre-rewrite reference
would let a mirror or alias redirect traffic to a blocked or plaintext host while
the check passed on the original name. registries.conf and policy.json are
trusted operator-controlled configuration; a threat model in which an attacker can
edit those files is out of scope (that host is already compromised).
Two consequences worth noting:
- Policy scope is repository-level. The
policy.jsonformat scopes rules toregistry[/namespace/repository]only — tags and digests are stripped before matching. A policy rule cannot protect (or single out) a specific tag or digest. - The trust policy is a pull-time gate. Manifest/index pulls are verified
against the policy; deletes are only checked against
blocked/insecure, never content-verified. Protecting a specific digest from deletion must be enforced by registry-side RBAC / tag immutability, not by the client trust policy.
The ORAS Java SDK can enforce a containers trust policy when pulling, using the
policy.json format used by Podman,
Skopeo and Buildah.
The policy is loaded from the following locations, in order (the first that exists wins):
$HOME/.config/containers/policy.json/etc/containers/policy.json
If no policy file is found, an accept-all policy is used. You can also set it explicitly:
// Load from a specific fileRegistryregistry = Registry.builder()
.defaults()
.withPolicy(Path.of("/etc/containers/policy.json"))
.build();
// Or build one programmaticallyRegistryregistry = Registry.builder()
.defaults()
.withPolicy(ContainersPolicy.rejectAll())
.build();When a policy is set, every manifest/index pull is evaluated against it and rejected
(OrasException) if it does not pass.
| Type | Supported | Behaviour |
|---|---|---|
insecureAcceptAnything | ✅ | Accept the image without any verification (trust all). |
reject | ✅ | Reject the image unconditionally. |
sigstoreSigned | ✅ | Accept only images with a valid keyed Sigstore (cosign) signature. |
signedBy (GPG) | ❌ | Not implemented. Legacy |
Only keyed verification is supported for now. If keyPath or keyData is present it contains a single
Sigstore public key (the cosign.pub produced by cosign generate-key-pair), and only signatures
made by that key are accepted:
keyPath— path to a PEM public key file.keyData— the same key, base64-encoded inline.
Multiple keys (keyPaths/keyDatas) and keyless (Fulcio/Rekor) verification are not supported.
Signatures are discovered through the OCI referrers API
(the Sigstore bundle, application/vnd.dev.sigstore.bundle.v0.3+json, attached to the image); no
local signature store is consulted. Verification binds the signature to the pulled image by its
digest. The signedIdentity field is not supported and is ignored if present, because the
cosign bundle payload carries only the image digest and no claimed Docker reference to match against.
{
"default": [{"type": "insecureAcceptAnything"}],
"transports": {
"docker": {
"example.com/my-image": [
{"type": "sigstoreSigned", "keyPath": "/home/me/my-key.pub"}
]
}
}
}SNAPSHOTS are automatically deployed when the main branch is updated. See the GitHub Actions for more details.
- Ensure the draft release version correspond to the version on the
pom.xml. Specially if changing the major or minor version. Patch releases are automatically updated. - Run the release workflow
Please note that this project has adopted the CNCF Code of Conduct. Please follow it in all your interactions with the project members and users.
This code is licensed under the Apache 2.0 LICENSE.