Conex, establish trust in community repositories

Written by hannes
Published: 2017-02-16 (last updated: 2023-11-20)

Less than two years after the initial proposal, we're happy to present conex 0.9.2. Pleas note that this is still work in progress, to be deployed with opam 2.0 and the opam repository.

screenshot

Conex is a library to verify and attest release integrity and authenticity of a community repository through the use of cryptographic signatures.

Packages are collected in a community repository to provide an index and allowing cross-references. Authors submit their packages to the repository. which is curated by a team of janitors. Information about a package stored in a repository includes: license, author, releases, their dependencies, build instructions, url, tarball checksum. When someone publishes a new package, the janitors integrate it into the repository, if it compiles and passes some validity checks. For example, its name must not be misleading, nor may it be too general.

Janitors keep an eye on the repository and fix emergent failures. A new compiler release, or a release of a package on which other packages depend, might break the compilation of a package. Janitors usually fix these problems by adding a patch to the build script, or introducing a version constraint in the repository.

Conex ensures that every release of each package has been approved by its author or a quorum of janitors. A conex-aware client initially verifies the repository using janitor key fingerprints as anchor. Afterwards, the on-disk repository is trusted, and every update is verified (as a patch) individually. This incremental verification is accomplished by ensuring all resources that the patch modifies result in a valid repository with sufficient approvals. Additionally, monotonicity is preserved by embedding counters in each resource, and enforcing a counter increment after modification. This mechanism avoids rollback attacks, when an attacker presents you an old version of the repository.

A timestamping service (NYI) will periodically approve a global view of the verified repository, together with a timestamp. This is then used by the client to prevent mix-and-match attacks, where an attacker mixes some old packages and some new ones. Also, the client is able to detect freeze attacks, since at least every day there should be a new signature done by the timestamping service.

The trust is rooted in digital signatures by package authors. The server which hosts the repository does not need to be trusted. Neither does the host serving release tarballs.

If a single janitor would be powerful enough to approve a key for any author, compromising one janitor would be sufficient to enroll any new identities, modify dependencies, build scripts, etc. In conex, a quorum of janitors (let's say 3) have to approve such changes. This is different from current workflows, where a single janitor with access to the repository can merge fixes.

Conex adds metadata, in form of resources, to the repository to ensure integrity and authenticity. There are different kinds of resources:

  • Authors, consisting of a unique identifier, public key(s), accounts.
  • Teams, sharing the same namespace as authors, containing a set of members.
  • Authorisation, one for each package, describing which identities are authorised for the package.
  • Package index, for each package, listing all releases.
  • Release, for each release, listing checksums of all data files.

Modifications to identities and authorisations need to be approved by a quorum of janitors, package index and release files can be modified either by an authorised id or by a quorum of janitors.

Documentation

API documentation is available online, also a coverage report.

We presented an abstract at OCaml 2016 about an earlier design.

Another article on an earlier design (from 2015) is also available.

Conex is inspired by the update framework, especially on their CCS 2010 paper, and adapted to the opam repository.

The TUF spec has a good overview of attacks and threat model, both of which are shared by conex.

What's missing

  • See issue 7 for a laundry list
  • Timestamping service
  • Key revocation and rollover
  • Tool to approve a PR (for janitors)
  • Camelus like opam-repository check bot
  • Integration into release management systems

Getting started

At the moment, our opam repository does not include any metadata needed for signing. We're in a bootstrap phase: we need you to generate a keypair, claim your packages, and approve your releases.

We cannot verify the main opam repository yet, but opam2 has support for a repository validation command, builtin, which should then call out to conex_verify (there is a --nostrict flag for the impatient). There is also an example repository which uses the opam validation command.

To reduce the manual work, we analysed 7000 PRs of the opam repository within the last 4.5 years (more details here. This resulted in an educated guess who are the people modifying each package, which we use as a basis whom to authorise for which packages. Please check with conex_author status below whether your team membership and authorised packages were inferred correctly.

Each individual author - you - need to generate their private key, submit their public key and starts approving releases (and old ones after careful checking that the build script, patches, and tarball checksum are valid). Each resource can be approved in multiple versions at the same time.

Installation

TODO: remove clone once PR 8494 is merged.

$ git clone -b auth https://github.com/hannesm/opam-repository.git repo
$ opam install conex
$ cd repo

This will install conex, namely command line utilities, conex_author and conex_verify_nocrypto/conex_verify_openssl. All files read and written by conex are in the usual opam file format. This means can always manually modify them (but be careful, modifications need to increment counters, add checksums, and be signed). Conex does not deal with git, you have to manually git add files and open pull requests.

Author enrollment

For the opam repository, we will use GitHub ids as conex ids. Thus, your conex id and your GitHub id should match up.

repo$ conex_author init --repo ~/repo --id hannesm
Created keypair hannesm.  Join teams, claim your packages, sign your approved resources and open a PR :)

This attempts to parse ~/repo/id/hannesm, errors if it is a team or an author with a publickey. Otherwise it generates a keypair, writes the private part as home.hannes.repo.hannesm.private (the absolute path separated by dots, followed by your id, and private - if you move your repository, rename your private key) into ~/.conex/, the checksums of the public part and your accounts into ~/repo/id/hannesm. See conex_author help init for more options (esp. additional verbosity -v can be helpful).

repo$ git status -s
 M id/hannesm

repo$ git diff //abbreviated output
-  ["counter" 0x0]
+  ["counter" 0x1]

-  ["resources" []]
+  [
+    "resources"
+    [
+      [
+        ["typ" "key"]
+        ["name" "hannesm"]
+        ["index" 0x1]
+        ["digest" ["SHA256" "ht9ztjjDwWwD/id6LSVi7nKqVyCHQuQu9ORpr8Zo2aY="]]
+      ]
+      [
+        ["typ" "account"]
+        ["name" "hannesm"]
+        ["index" 0x2]
+        ["digest" ["SHA256" "aCsktJ5M9PI6T+m1NIQtuIFYILFkqoHKwBxwvuzpuzg="]]
+      ]
+
+keys: [
+  [
+    [
+      "RSA"
+      """
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyUhArwt4XcxLanARyH9S
...
+9KQdg6QnLsQh/j74QKLOZacCAwEAAQ==
+-----END PUBLIC KEY-----"""
+      0x58A3419F
+    ]
+    [
+      0x58A79A1D
+      "RSA-PSS-SHA256"
+      "HqqicsDx4hG9pFM5E7"
+    ]
+  ]
+]

Status

If you have a single identity and contribute to a single signed opam repository, you don't need to specify --id or --repo from now on.

The status subcommand presents an author-specific view on the repository. It lists the own public keys, team membership, queued resources, and authorised packages.

The opam repository is in a transitionary state, we explicitly pass --quorum 0, which means that every checksum is valid (approved by a quorum of 0 janitors).

repo$ conex_author status --quorum 0 arp
author hannesm #1 (created 0) verified 3 resources, 0 queued
4096 bit RSA key created 1487094175 approved, SHA256: ht9ztjjDwWwD/id6LSVi7nKqVyCHQuQu9ORpr8Zo2aY=
account GitHub hannesm approved
account email hannes@mehnert.org approved
package arp authorisation approved
conex_author: [ERROR] package index arp was not found in repository

This shows your key material and accounts, team membership and packages you are authorised to modify (inferred as described here.

The --noteam argument limits the package list to only these you are personally authorised for. The --id argument presents you with a view of another author, or from a team perspective. The positional argument is a prefix matching on package names (leave empty for all).

Resource approval

Each resource needs to be approved individually. Each author has a local queue for to-be-signed resources, which is extended with authorisation, init, key, release, and team (all have a --dry-run flag). The queue can be dropped using conex_author reset. Below shown is conex_author sign, which let's you interactively approve queued resources and cryptopgraphically signs your approved resources afterwards.

The output of conex_author status listed an authorisation for conf-gsl, which I don't feel responsible for. Let's drop my privileges:

repo$ conex_author authorisation conf-gsl --remove -m hannesm
modified authorisation and added resource to your queue.

I checked my arp release careful (checksums of tarballs are correct, opam files do not execute arbitrary shell code, etc.), and approve this package and its single release:

repo$ conex_author release arp
conex_author.native: [WARNING] package index arp was not found in repository
conex_author.native: [WARNING] release arp.0.1.1 was not found in repository
wrote release and added resources to your queue.

Once finished with joining and leaving teams (using the team subcommand), claiming packages (using the authorisation subcommand), and approve releases (using the release subcommand), you have to cryprographically sign your queued resource modifications:

repo$ conex_author sign
release arp.0.1.1 #1 (created 1487269425)
[descr: SHA256: aCsNvcj3cBKO0GESWG4r3AzoUEnI0pHGSyEDYNPouoE=;
opam: SHA256: nqy6lD1UP+kXj3+oPXLt2VMUIENEuHMVlVaG2V4z3p0=;
url: SHA256: FaUPievda6cEMjNkWdi0kGVK7t6EpWGfQ4q2NTSTcy0=]
approved (yes/No)?
package arp #1 (created 1487269425) [arp.0.1.1]
approved (yes/No)?y
authorisation conf-gsl #1 (created 0) empty
approved (yes/No)?y
wrote hannesm to disk

repo$ conex_author status --quorum 0 arp
author hannesm #1 (created 0) verified 7 resources, 0 queued
4096 bit RSA key created 1487094175 approved, SHA256: ht9ztjjDwWwD/id6LSVi7nKqVyCHQuQu9ORpr8Zo2aY=
account GitHub hannesm approved
account email hannes@mehnert.org approved
package arp authorisation approved package index approved
release arp.0.1.1: approved

If you now modify anything in packages/arp (add subdirectories, modify opam, etc.), this will not be automatically approved (see below for how to do this).

You manually need to git add some created files.

repo$ git status -s
 M id/hannesm
 M packages/conf-gsl/authorisation
?? packages/arp/arp.0.1.1/release
?? packages/arp/package

repo$ git add packages/arp/arp.0.1.1/release packages/arp/package
repo$ git commit -m "hannesm key enrollment and some fixes" id packages

Now push this to your fork, and open a PR on opam-repository!

Editing a package

If you need to modify a released package, you modify the opam file (as before, e.g. introducing a conflict with a dependency), and then approve the modifications. After your local modifications, conex_author status will complain:

repo$ conex_author status arp --quorum 0
package arp authorisation approved package index approved
release arp.0.1.1: checksums for arp.0.1.1 differ, missing on disk: empty, missing in checksums file: empty, checksums differ: [have opam: SHA256: QSGUU9HdPOrwoRs6XJka4cZpd8h+8NN1Auu5IMN8ew4= want opam: SHA256: nqy6lD1UP+kXj3+oPXLt2VMUIENEuHMVlVaG2V4z3p0=]

repo$ conex_author release arp.0.1.1
released and added resources to your resource list.

repo$ conex_author sign
release arp.0.1.1 #1 (created 1487269943)
[descr: SHA256: aCsNvcj3cBKO0GESWG4r3AzoUEnI0pHGSyEDYNPouoE=;
opam: SHA256: QSGUU9HdPOrwoRs6XJka4cZpd8h+8NN1Auu5IMN8ew4=;
url: SHA256: FaUPievda6cEMjNkWdi0kGVK7t6EpWGfQ4q2NTSTcy0=]
approved (yes/No)? y
wrote hannesm to disk

The release subcommand recomputed the checksums, incremented the counter, and added it to your queue. The sign command signed the approved resource.

repo$ git status -s
 M id/hannesm
 M packages/arp/arp.0.1.1/opam
 M packages/arp/arp.0.1.1/package

repo$ git commit -m "fixed broken arp package" id packages

Janitor tools

Janitors need to approve teams, keys, accounts, and authorisations.

To approve resources which are already in the repository on disk, the key subcommand queues approval of keys and accounts of the provided author:

repo$ conex_author key avsm
added keys and accounts to your resource list.

The authorisation subcommand, and team subcommand behave similarly for authorisations and teams.

Bulk operations are supported as well:

conex_author authorisation all

This will approve all authorisations of the repository which are not yet approved by you. Similar for the key and team subcommands, which also accept all.

Don't forget to conex_author sign afterwards (or yes | conex_author sign).

Verification

The two command line utlities, conex_verify_openssl and conex_verify_nocrypto contain the same logic and same command line arguments.

For bootstrapping purposes (nocrypto is an opam package with dependencies), conex_verify_openssl relies on the openssl command line tool (version 1.0.0 and above) for digest computation and verification of the RSA-PSS signature.

The goal is to use the opam2 provided hooks, but before we have signatures we cannot enable them.

See the example repository for initial verification experiments, and opam2 integration.

I'm interested in feedback, please open an issue on the conex repository. This article itself is stored as Markdown in a different repository.