Signed package registry backed by the Inter-Planetary File System for storage.
Minimum supported rust version (MSRV) is 1.63.0.
Content addressing used by the Inter-Planetary File System (IPFS) network is a good fit for a package registry as it prevents packages from being tampered with and provides decentralized storage of the package archives.
However, there is a tension between using opaque identifiers and exposing human-friendly references to packages. To resolve this we support both types of references so that callers can choose from tamper proof in the case of an opaque Content Identifier (CID) or from tamper protected in the case of a human-readable package reference.
For example, to fetch a package from the registry using a CID such as:
/ipfs/QmSYVWjXh5GCZpxhCSHMa89X9VHnPpaxafkBAR9rjfCenb
Can be said to be tamper proof as changing the package contents would also change the CID.
This kind of package reference does not tell us anything about the package name or version which may not be useful depending upon the use case. The registry will also accept a pointer to a package such as:
mock-namespace/mock-package/1.0.0
In this case the registry will look up the object key in a database before returning the package file. Using a pointer reference is said to be tamper protected because it is possible for the registry operator to change the object key.
In the future support can be added to mitigate this by storing object references in a smart contract ensuring that both kinds of package references are tamper proof.
Using Elliptic Curve Digital Signature Algorithm (ECDSA) recoverable signatures to identify publishers avoids storing any Personally Identifiable Information (PII) and allows us to add Self-Sovereign Identity (SSI) support enabling publishers to verify their identity using Distributed Identifiers (DID) and Verifiable Credentials (VC).
All packages must be signed so we establish irrefutable proof of which identity published a package.
Clients could in the future support Multi-Party Computation (MPC) for package publishing which would allow organizations to ensure multiple parties are involved in signing off a package helping increase the security of the supply chain.
A key feature of any package registry is the ability to discover packages; meta data about the published packages is stored in a database and exposed via a public Application Programming Interface (API).
The package registry supports multiple storage layers so it can be configured to automatically mirror published packages; see storage configuration for more information.
Namespaces are useful as a means to establish trust for a collection of packages and to allow publishers to name their packages without collisions.
They don't prevent name-squatting as that problem just moves from the package name level to the namespace level; but they do help to make it easier to identify the author(s) of a package so we designed the registry with namespaces baked in.
Namespaces take precedence over a scope in npm package definitions; scopes are ignored.
The only thing the registry needs to extract from a package archive is the package name and semver so it can easily support different kinds of packages.
Currently support is provided for npm packages (the default) as well as crates generated by cargo package
; let us know if you have a package archive format that you would like to support.
To mitigate identifier based attacks all namespace and package names are subject to the unicode security mechanisms; mixed script and confusable detection is thanks to the unicode security crate.
- Identifier MUST be at least three characters in length
- Identifier MUST have an alphabetic first character
- Identifier MUST NOT contain ASCII control characters
- Identifier MUST NOT contain ASCII punctuation (except for the hyphen)
- Identifier MUST NOT contain emojis
- Identifier MUST NOT contain invisible characters
- Identifier MUST conform to the general security profile, see general security profile
- Identifier MUST be a single script, see single script
Namespaces and packages store a confusable skeleton in the database and comparison is performed on the skeleton when retrieving namespaces and packages by identifier which provides some protection for registering identifiers that are confusable, see confusables.
Organizations need to manage multiple signing keys and possibly restrict access to certain packages as well as support publishing in Continuous Integration / Continuous Deployment (CI/CD) pipelines.
The owner of a namespace has complete control and can create administrators and users.
Administrators can publish to all packages as well as add and remove other non-administrator users.
Users with no access restrictions can publish to all packages; if package access restrictions have been applied then publishing is restricted to the allowed list of packages.
Package registry operators may wish to augment the core functionality with additional features, here are some ideas:
- Static website to browse the packages
- Archive browser such as npmfs
- Generate API documentation like docs.rs
- Static analysis for attack detection
- Produce a Software Bill of Materials (SBOM)
- Maintain an index of packages for a search engine
- Compile usage statistics
To support these use cases the server implements webhooks allowing registry operators to extend the server functionality for their needs. See the webhooks configuration section for more information.
If you already have rust installed you may need to update the stable channel first if you don't already have >= 1.63.0
:
rustup update stable
Install the binary:
cargo install --path .
Ensure a local IPFS node is running:
ipfs daemon
Start the server:
ipkg server -c ./sandbox/config.toml
Generate a signing key; you will be prompted to choose a password for the keystore:
ipkg keygen ./sandbox
Signup so the public key is registered for publishing:
ipkg signup -k ./sandbox/<addr>.json
Replace <addr>
with the address of the public key and enter the password for the keystore when prompted.
Register a namespace for published packages:
ipkg register -k ./sandbox/<addr>.json mock-namespace
Publish a package:
ipkg publish -k ./sandbox/<addr>.json -n mock-namespace fixtures/mock-package-1.0.0.tgz
Download the package to a file using a content ID:
ipkg fetch /ipfs/QmSYVWjXh5GCZpxhCSHMa89X9VHnPpaxafkBAR9rjfCenb sandbox/package.tgz
Or alternatively using a pointer:
ipkg fetch mock-namespace/mock-package/1.0.0 sandbox/package.tgz
Get information about a namespace, package or version:
ipkg get mock-namespace
ipkg get mock-namespace/mock-package
ipkg get mock-namespace/mock-package/1.0.0
ipkg get /ipfs/QmSYVWjXh5GCZpxhCSHMa89X9VHnPpaxafkBAR9rjfCenb
List packages and versions:
ipkg ls mock-namespace
ipkg ls mock-namespace/mock-package
ipkg ls mock-namespace --latest
ipkg ls mock-namespace/mock-package --range '=1.0.0'
Some environment variables modify the behavior of the CLI. Use IPKG_KEYSTORE
as a shortcut for the --key
option.
So that publishing is possible from CI/CD pipelines the IPKG_KEYSTORE_PASSWORD
variable will use the specified password instead of prompting for a password interactively. Do not use outside of CI/CD environments.
IPKG_KEYSTORE
: Path to a signing keystore.IPKG_KEYSTORE_PASSWORD
: Password for a signing keystore.
For API calls that require authentication the x-signature
header MUST be a base64 encoded string of a 65-byte Ethereum-style ECDSA recoverable signature.
POST /api/publisher
Register a signing key for publishing.
x-signature
: Signature of the well known value.ipfs-registry
.
{
"address": "0x1fc770ac21067a04f83101ebf19a670db9e3eb21",
"created_at": "2022-09-11T08:28:17Z"
}
POST /api/namespace/:namespace
Register a namespace; if the namespace already exists a 409 CONFLICT response is returned.
x-signature
: Signature of the bytes for:namespace
.
{
"name": "mock-namespace",
"owner": "0x1fc770ac21067a04f83101ebf19a670db9e3eb21",
"created_at": "2022-09-11T08:29:27Z"
}
POST /api/namespace/:namespace/user/:address
Add a user to a namespace.
If the user already has access to the namespace a 409 CONFLICT response is returned.
If the address of the signer has been denied then a 401 UNAUTHORIZED response is returned.
admin
: Boolean indicating the user is an administrator (default:false
).package
: Optional name of a package restriction for the new user.
x-signature
: Signature of the bytes for:address
.
200 if successful.
DELETE /api/namespace/:namespace/user/:address
Remove a user from a namespace.
If the address of the signer has been denied then a 401 UNAUTHORIZED response is returned.
x-signature
: Signature of the bytes for:address
.
200 if successful.
POST /api/package/:namespace
If the package already exists or is not ahead of the latest version a 409 CONFLICT response is returned.
If the address of the signer has been denied then a 401 UNAUTHORIZED response is returned.
The default configuration limits requests to 16MiB so if the package is too large a 413 PAYLOAD TOO LARGE response is returned.
:namespace
: The package namespace.
x-signature
: Signature of the bytes for the request body.content-type
: Should match the MIME type for the registry (default:application/gzip
)
{
"id": "mock-namespace/mock-package/1.0.0",
"artifact": {
"namespace": "mock-namespace",
"package": {
"name": "mock-package",
"version": "1.0.0"
}
},
"key": "/ipfs/QmSYVWjXh5GCZpxhCSHMa89X9VHnPpaxafkBAR9rjfCenb",
"checksum": "4ad90a2c2e08374f8ccec2b604915a0ab7e97fcca983b12a6857d20df3fca9c0"
}
GET /api/package?id=<package-id>
To download a package construct a URL containing the package identifier; the identifier may be an IPFS reference such as:
/ipfs/QmSYVWjXh5GCZpxhCSHMa89X9VHnPpaxafkBAR9rjfCenb
Or a package pointer:
mock-namespace/mock-package/1.0.0
id
: Package identifier.
GET /api/package/:namespace/packages
List the packages for a namespace.
:namespace
: The package namespace.
include
: Fetch versions for each package, eithernone
orlatest
. Default isnone
.limit
: Limit per page.offset
: Offset for pagination.sort
: Sort order, eitherasc
ordesc
.
{
"records": [
{
"name": "mock-package",
"created_at": "2022-09-11T08:30:27Z"
}
],
"count": 1
}
GET /api/package/:namespace/:package/versions
List the versions of a package.
:namespace
: The package namespace.:package
: The package name.
range
: Version range query, see semver crate for details.limit
: Limit per page.offset
: Offset for pagination.sort
: Sort order, eitherasc
ordesc
.
{
"records": [
{
"version": "1.0.0",
"content_id": "QmSYVWjXh5GCZpxhCSHMa89X9VHnPpaxafkBAR9rjfCenb",
"pointer_id": "230e83dd43123aa0f3d8bc337b0f63440a6128ae8491ee70f42d02594c087d49",
"signature": "mgtkUNH0I4D4JqhvLYEG1snbBByRLZCmBj5r+KKJiTAVUdBFj7Sm9JtGczTX0dk2jjtBH0wbLOcFIWesQiwVAwE=",
"checksum": "4ad90a2c2e08374f8ccec2b604915a0ab7e97fcca983b12a6857d20df3fca9c0",
"created_at": "2022-09-14T01:19:12Z"
},
{
"version": "1.0.1",
"content_id": "QmQfiqgpEL7gWavVJ5r2JK17N516q9wWoL8eHjwq8zKozZ",
"pointer_id": "f52b51ea3b48652b6c01892695b92c76c404a5efe8270a331e981a3b1f772b47",
"signature": "2tBILxCWrZ8MrvXUIudC/0BSxJf8+gor6tJWiGxxN3A1e/gQKXrtFo4+CIjn4Atz8uUNQyrdzZtG6+/wPuPQbAA=",
"checksum": "6fb6f92379c52eeb7f18d56c6fc745755588ebbccd5db0e157c9938daaf5e359",
"created_at": "2022-09-14T01:19:17Z"
},
{
"version": "2.0.0-alpha.1",
"content_id": "QmbptdWzd7pzNbmTkGwtYRdQWYCmXYjQ6tJV9CkWkjD2V8",
"pointer_id": "ed7cfb288b5b7dedaa4dd2e189e921d839cc832d39d13d8a2be87c6b340809fb",
"signature": "krOfiqyqEJ4TYZTJikfnQdBYxqGwQv4EU/JKrt64eLVJnDiBYqSOVfH4h1bpc1ghrCb4S323UGDgrCytHc43swA=",
"checksum": "58313c4525d2253048a7b7342bb63b4a914bd5ae2ee5eab9e22f35c8897b5db5",
"created_at": "2022-09-14T01:19:30Z"
}
],
"count": 3
}
GET /api/package/:namespace/:package/latest
Get the latest version of a package.
:namespace
: The package namespace.:package
: The package name.
prerelease
: Whentrue
include prerelease versions.
Response with ?prerelease=true
query string:
{
"version": "2.0.0-alpha.1",
"package": {
"author": "",
"description": "Mock package to test NPM registry support",
"license": "ISC",
"main": "index.js",
"name": "mock-package",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"version": "2.0.0-alpha.1"
},
"content_id": "QmbptdWzd7pzNbmTkGwtYRdQWYCmXYjQ6tJV9CkWkjD2V8",
"pointer_id": "ed7cfb288b5b7dedaa4dd2e189e921d839cc832d39d13d8a2be87c6b340809fb",
"signature": "krOfiqyqEJ4TYZTJikfnQdBYxqGwQv4EU/JKrt64eLVJnDiBYqSOVfH4h1bpc1ghrCb4S323UGDgrCytHc43swA=",
"checksum": "58313c4525d2253048a7b7342bb63b4a914bd5ae2ee5eab9e22f35c8897b5db5",
"created_at": "2022-09-14T01:19:30Z"
}
GET /api/package/version?id=<package-id>
Get a specific version of a package.
id
: Package identifier.
See example response for latest version above.
POST /api/package/yank?id=<package-id>
Mark a specific version of a package as yanked.
The body should be a UTF-8 encoded string of the reason why the version was yanked; it may be the empty string.
id
: Package identifier.
x-signature
: Signature of the bytes for the request body.
200 if successful.
POST /api/package/:namespace/:package/deprecate
Mark a package as deprecated.
The body should be a UTF-8 encoded string of a deprecation notice; it may be the empty string.
:namespace
: The package namespace.:package
: The package name.
x-signature
: Signature of the bytes for the request body.
200 if successful.
This section describes the server configuration; after making changes to the configuration you must restart the server for changes to take effect.
The default database is an in-memory sqlite database; to configure a file on disc for the database:
[database]
url = "sqlite:ipfs_registry.db"
In the future we intend to support a postgres database driver too.
Storage for packages is defined as an ordered set of layers.
You must define at least one layer; to define an IPFS layer specify an object with a url
field that points to the node URL.
[storage]
layers = [
{ url = "https://ipfs-node1.example.com" }
]
For example, to mirror to multiple IPFS nodes:
[storage]
layers = [
{ url = "https://ipfs-node1.example.com" },
{ url = "https://ipfs-node2.example.com" },
{ url = "https://ipfs-node3.example.com" },
]
To define a storage layer backed by an AWS S3 bucket you must specify the profile
, region
and bucket
; the profile
must be a valid profile in ~/.aws/credentials
with read and write permissions for the bucket.
[storage]
layers = [
{ region = "ap-southeast-1", profile = "example", bucket = "registry.example.com" }
]
When using an AWS S3 bucket as a storage layer in production it is strongly recommended that the bucket has versioning and object locks enabled. Mixing layers is encouraged for redundancy:
[storage]
layers = [
{ url = "https://ipfs-node1.example.com" },
{ region = "ap-southeast-1", profile = "example", bucket = "registry.example.com" },
]
Local filesystem storage can be configured using a file layer:
[storage]
layers = [
{ directory = "./objects" },
]
Relative paths are resolved from the directory containing the configuration file; the path must be a directory.
Note that all the downstream storage layers must be available for the service to work as intended; ie, requests must succeed across all storage layers for the server to return a success response.
Set the registry kind
to determine how package data is extracted from package archives when they are published.
[registry]
kind = "cargo"
Supported registry kinds are:
If you need to allow packages larger than the default 16MiB use body-limit
:
[registry]
body-limit = 33554432 # 32MiB
To restrict access to an allowed list of publishers specify addresses in the allow
set:
[registry]
allow = [
"0x1fc770ac21067a04f83101ebf19a670db9e3eb21"
]
To deny publish access use the deny
set:
[registry]
deny = [
"0x1fc770ac21067a04f83101ebf19a670db9e3eb21"
]
To configure services to receive webhook events list the endpoints and configure a signing key.
[webhooks]
endpoints = [
"http://localhost:5555"
]
key = "./0x1fc770ac21067a04f83101ebf19a670db9e3eb21.json"
retry-limit = 5
backoff-seconds = 30
When webhooks are configured the server MUST be started with an IPKG_WEBHOOK_KEYSTORE_PASSWORD
environment variable which provides the password for the webhook signing keystore. If this variable is not set or is incorrect the server will fail to start.
Each configured endpoint is sent a POST request with a JSON document as the body:
{ "event": "publish", "body": {} }
The content of the body
will depend upon the webhook event, supported events are:
publish
: When a package is published.fetch
: When a package is downloaded.
All webhook requests are signed using the provided key and the signature is sent in the x-signature
header; services receiving webhook events SHOULD check the signature against the expected address to verify the request origin.
Backoff logic for webhook events is exponential. Registry operators should take care to ensure downstream webhook services have high availability otherwise it may put too much pressure on the server under high load.
The default CORS configuration is very permissive, if you wish to restrict to certain origins:
[cors]
origins = [
"https://example.com"
]
To run the server over HTTPS specify certificate and key files:
[tls]
cert = "cert.pem"
key = "key.pem"
Relative paths are resolved from the directory containing the configuration file.
Install sqlx
and cargo make
:
cargo install sqlx-cli
cargo install cargo-make
Then create a .env
file from .env.example
. Afterwards, create a database and run the migrations:
cargo make dev-db
Typical workflow is to run the test suite and format the code:
cargo make dev
Starting a local server (requires an IPFS node running locally):
cargo make dev-server
To test TLS support for IPFS nodes, set up CORS for https://localhost
:
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["https://localhost", "http://localhost:3000", "http://127.0.0.1:5001", "https://webui.ipfs.io"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST"]'
Then install and run caddy as a reverse proxy:
caddy reverse-proxy --to 127.0.0.1:5001
Make sure you can view https://localhost/webui
and then create a configuration that connects to IPFS over HTTPS:
[database]
url = "sqlite:ipfs_registry.db"
[storage]
layers = [
{ url = "https://localhost" }
]
And start the server:
cargo run -- server -c sandbox/ipfs-tls.toml
MIT or Apache-2.0