apk #
The formats Alpine's package manager reads, in pure OCaml: the APKINDEX a
repository publishes, the concatenated gzip streams a .apk file is, the RSA
signature over the member that follows the first of them, and the dependency
closure an install of a set of roots pulls in. No apk binary is run.
A tool that builds an Alpine root filesystem, or a mirror that serves one, needs to read the same metadata as apk-tools. apk-tools is written in C and expects an Alpine system; this library reads apk v2 metadata on any host that runs OCaml.
Installation #
Install with opam:
$ opam install apk
If opam cannot find the package, it has probably not reached the public
opam-repository yet. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install apk
Reading an index #
An APKINDEX is stanzas of <letter>:<value> lines separated by blank lines.
A package keeps its stanza in the order it was read, so an index goes back out
as it came in.
The stanza below is one package of a real index, C: first and a blank line
after it. P: is the name, V: the version, A: the architecture and D:
a dependency. C: is the checksum of the package's control stream (see
Verifying an archive): the characters Q1,
apk-tools' marker for a base64-encoded 20-byte hash, followed by the base64
of the SHA-1 of that stream's raw bytes.
C:Q1EyN5AdpAOBJWKMR89pp/C66o+OE=
P:busybox
V:1.37.0-r18
A:aarch64
D:so:libc.musl-aarch64.so.1
# let index =
"C:Q1EyN5AdpAOBJWKMR89pp/C66o+OE=\nP:busybox\nV:1.37.0-r18\nA:aarch64\nD:so:libc.musl-aarch64.so.1\n\n";;
val index : string =
"C:Q1EyN5AdpAOBJWKMR89pp/C66o+OE=\nP:busybox\nV:1.37.0-r18\nA:aarch64\nD:so:libc.musl-aarch64.so.1\n\n"
# let packages = Apk.Index.packages (Apk.Index.of_string_exn index);;
val packages : Apk.Index.pkg list = [<abstr>]
# List.map Apk.Index.name packages;;
- : string list = ["busybox"]
# List.map (fun p -> Option.value ~default:"" (Apk.Index.arch p)) packages;;
- : string list = ["aarch64"]
# Apk.Index.to_string (Apk.Index.of_string_exn index) = index;;
- : bool = true
Resolving a closure #
Closure.One_provider is what a host installing a root filesystem itself
takes: one package per dependency name. Closure.Every_provider is what a
guest's own apk may ask a mirror for: every provider of a name, closed under
the index's install-if entries. Several packages may provide one name (a
shared-library so: name or a virtual package), and each is a provider of
it. An install-if entry (i: in the index) says a package is installed
automatically as soon as every package it lists is installed: below,
extra joins once lib and app are both in.
# let index =
Apk.Index.of_string_exn
"P:app\nV:1\nD:lib\n\nP:lib\nV:1\n\nP:extra\nV:1\ni:lib app\n\n";;
val index : Apk.Index.t = <abstr>
# let roots = [ Apk.Dep.of_string_exn "app" ];;
val roots : Apk.Dep.t list = [<abstr>]
# let names breadth =
Apk.Closure.resolve breadth (Apk.Index.packages index) ~roots
|> Result.map (List.map Apk.Index.name);;
val names : Apk.Closure.breadth -> (string list, [> `Msg of string ]) result =
<fun>
# names Apk.Closure.One_provider;;
- : (string list, [> `Msg of string ]) result = Ok ["app"; "lib"]
# names Apk.Closure.Every_provider;;
- : (string list, [> `Msg of string ]) result = Ok ["app"; "extra"; "lib"]
Verifying an archive #
An apk v2 package is three gzip streams one after the other: a signature
stream holding one file .SIGN.RSA.<keyid> (or .SIGN.RSA256.), a control
stream holding .PKGINFO, and a data stream with the package's files. An
APKINDEX.tar.gz opens the same way, with the index in its second stream.
Archive.signed splits the first two streams and hands back the raw bytes
of the second. The signature is RSA PKCS#1 v1.5 over the SHA-1 (RSA),
SHA-256 (RSA256) or SHA-512 (RSA512) of those raw, still-compressed
bytes. The data stream is covered only indirectly: .PKGINFO records its
SHA-256 as datahash, which apk-tools checks on install and this library
does not check. Signature.verify checks that signature against
the key it names in a directory of trusted keys, /etc/apk/keys on a real
system.
# let verified ~keys_dir archive =
match Apk.Archive.signed archive with
| Error _ as e -> e
| Ok (signature, control) ->
Apk.Signature.verify ~keys_dir
~signature:(Apk.Archive.data signature)
~signed:(Apk.Archive.raw control)
|> Result.map (fun () -> Apk.Archive.data control);;
val verified :
keys_dir:Fpath.t -> string -> (string, [> `Msg of string ]) result = <fun>
# verified ~keys_dir:(Fpath.v "/etc/apk/keys") "not an archive at all";;
- : (string, [> `Msg of string ]) result =
Error (`Msg "malformed gzip member: Invalid GZip header")
The index a repository publishes also vouches for each package's control
member: Archive.control_checksum spells the C: value a stanza carries.
Standards #
- apk-v2(5): the archive, its three sections, and the
.SIGN.<algorithm>.<keyid>signature members. - apk-package(5): the metadata fields, each named by its v3 name, its
.PKGINFOname and its index character. - apk-keys(5): the directory a signature's key name is resolved in.
The index and closure test vectors are apk-tools' own solver test data, taken verbatim.
Related work #
apk-tools is the reference implementation and is C. abuild writes the
archives it reads. No OCaml library implements either format: opam carries no
apk package, and this library builds on the existing OCaml implementations of
the layers underneath (tar, gzip and RSA PKCS#1) instead of reimplementing
them. It reads v2 only;
the v3 ADB format that apk-tools 3 introduces is a different encoding and is
not handled here.
Licence #
ISC, see LICENSE.md.