DPKG软件仓库搭建

dpkg软件包管理工具往往部署于Debian系系统,像Ubuntu、Termux等发行版均使用dpkg来管理软件包。本文介绍dpkg软件仓库的搭建,并介绍apt是如何获取和校验软件包信息的。

准备

制作DPKG软件包

打包一个DPKG软件包,可以参考上一篇文章:

注意软件包的命名格式最好为foo-bar_1.0.1-1_amd64.deb

DPKG软件仓库结构

DPKG软件仓库主要由索引和二进制软件包组成。索引存放于dists目录,包含了仓库中所有软件包的信息,包括校验值等。二进制软件包存放于pool目录,即为已经打包为deb文件的软件包。

一个DPKG软件仓库中的索引是分类存放的,首先可以分成若干个发布版(releases/code name),比如我们熟知的Ubuntu的bionic、focal等;每个发行版还可继续分不同套件(suites/components),比如Ubuntu软件仓库分为main、multiverse、universe等;每个套件内再继续分不同类型和架构(architectures),如binary-amd64、binary-i386、source等。

如我们常使用的APT配置sources.list中,是这样写的:

deb https://packages.example.com/ubuntu focal main universe multiverse

实际上,在获取索引的时候,就会获取以下文件:

  • https://packages.example.com/ubuntu/dists/focal/Release
  • https://packages.example.com/ubuntu/dists/focal/main/binary-amd64/Packages.gz
  • https://packages.example.com/ubuntu/dists/focal/universe/binary-amd64/Packages.gz
  • https://packages.example.com/ubuntu/dists/focal/multiverse/binary-amd64/Packages.gz

我们可以下载软件仓库的相关文件看看,可以看出Release文件中存储了当前发布版仓库的所有信息,以及不同套件目录下各文件的校验值。

软件包也可以分类存放,但通常使用首字母来分类如a、b、liba、libb等,当然也可以直接使用软件包名称命名。具体如何组织目录无需特别注意,因为后面会讲到,构建目录仓库时会将自动相关二进制包的路径写入Packages这个索引文件中。

总体来说,一个DPKG软件仓库结构如下:

packages.example.com/
|-- ubuntu/
    |-- dists/
    |   |-- bionic/
    |       |-- multiverse/
    |       |   |-- binary-amd64/
    |       |       |-- Packages
    |       |-- Release
    |-- pool/
        |-- abc/
            |-- abc_1.0.1-1_amd64.deb
            |-- abc-dev_1.0.1-1_amd64.deb

创建DPKG软件仓库

通过上一节对DPKG软件仓库的介绍,你也许稍微明白了从软件仓库在线安装软件包的具体流程,即APT根据当前系统的架构,首先获取Release文件这个总索引并校验,然后分别下载各个套件的Packages文件并校验,最后根据用户提供的包名,在各个Packages文件中查找,找到则请求相应地址,从服务器获取相应的包进行安装。因此可以看出,软件仓库的搭建本质上就是为各个软件包建立索引文件,并为其签名保证安全性。

Packages文件生成

如果我们只需要搭建一个简单的仓库,则无需手动为每个软件包建立索引,使用一个命令就可以了。

cd example-repo/my-lfs-os/
mkdir -p dists/osrelease-1.1/multiverse/binary-amd64
dpkg-scanpackages --arch amd64 pool/ > dists/osrelease-1.1/multiverse/binary-amd64/Packages
cat dists/osrelease-1.1/multiverse/binary-amd64/Packages | gzip -9 > dists/osrelease-1.1/multiverse/binary-amd64/Packages.gz  #gzip压缩,可选
cat dists/osrelease-1.1/multiverse/binary-amd64/Packages | xz -9 > dists/osrelease-1.1/multiverse/binary-amd64/Packages.xz  #xz压缩,可选

dpkg-scanpackages命令可以自动扫描指定的目录下所有DPKG软件包,将包元数据(DEBIAN/control文件)提取出来,加上路径和校验值等信息,一起写入Packages文件。

注意:随着软件更新,在pool中可以出现相同名称、不同版本的软件。此时若再执行以上命令,会出现错误。因为dpkg-scanpackages默认不允许出现重复的软件,即使它们版本不同。要解决这一问题,可以有两种方法:第一种是添加–multiversion参数,允许存在不同版本的同名软件,apt安装软件时,也会选择最高版本的安装;第二种是循规蹈矩,仅将特定版本的软件纳入Packages文件中。如果你仅想搭建一个简单的软件仓库,用来发布自己的软件,那么第一种方法足够了;而对于像Ubuntu软件仓库这种大型仓库,由于希望防止用户意外安装未测试的新版软件来避免导致的一系列问题,应选择第二种方法。

生成的Packages文件例子如下:

Package: libmewwoof-hello
Version: 0.1.0-1
Architecture: amd64
Maintainer: Johnson Liu <johnson@awaworks.net>
Depends: libc6
Filename: pool/main/libmewwoof-hello/libmewwoof-hello_0.1.0-1_amd64.deb
Size: 2332
MD5sum: 30dac06203a11963ff001050b981a708
SHA1: cf634c50e3998b4bc27dcdff7361b5fb3d0d8400
SHA256: 642b18e1629110bf2e3452e8dd5ccda52144072932b619248c9baa87abf8a815
Homepage: https://www.mewwoof.cn
Description: A program that prints hello - Library

Package: ...

Release文件生成

Release文件存储的是整个发布版索引文件的索引,包括它们的校验码。简单点说就是对dists/$code_name下所有文件的索引。目前使用最广泛的是InRelease文件,该文件由Release文件用OpenPGP加密而来。对于不支持InRelease的老设备,也可提供Release.gpg这样分离的OpenPGP签名文件。

本小节主要介绍Release文件,为该文件签名的流程可见下一节。

可以从Ubuntu官方仓库下载Release文件,可以看到以下的结构:

Origin: Ubuntu
Label: Ubuntu
Suite: bionic
Version: 18.04
Codename: bionic
Date: Thu, 26 Apr 2018 23:37:48 UTC
Architectures: amd64 arm64 armhf i386 ppc64el s390x
Components: main restricted universe multiverse
Description: Ubuntu Bionic 18.04
MD5Sum:
 32a92a5c20f378d42dd2d2f4f28f6637        628836439 Contents-amd64
 53c6a594819b51a5755f88b45d1eff7f         37766986 Contents-arm64.gz
...

对于Release文件,虽然并没有硬性要求必须要记录哪些属性,但有一些还是建议一定要有的:

  • Suite/Codename
  • Architectures
  • Components
  • Date
  • SHA256

但一些其他属性也建议保留,按照Ubuntu软件仓库的文件来做就没错了。

可以看到Release文件的生成并没有什么好方法,不过我们可以写一个脚本来生成:

(代码参考来源:https://earthly.dev/blog/creating-and-hosting-your-own-deb-packages-and-apt-repo/

#!/bin/sh
set -e

do_hash() {
    HASH_NAME=$1
    HASH_CMD=$2
    echo "${HASH_NAME}:"
    for f in $(find -type f); do
        f=$(echo $f | cut -c3-) # remove ./ prefix
        if [ "$f" = "Release" ]; then
            continue
        fi
        echo " $(${HASH_CMD} ${f}  | cut -d" " -f1) $(wc -c $f)"
    done
}

cat << EOF
Origin: Example Repository
Label: Example
Suite: stable
Codename: stable
Version: 1.0
Architectures: amd64 aarch64
Components: main
Description: An example software repository
Date: $(date -Ru)
EOF

do_hash "MD5Sum" "md5sum"
do_hash "SHA1" "sha1sum"
do_hash "SHA256" "sha256sum"

进入$codename目录,执行脚本,即可完成构建:

cd repo-root/dists/my-code-name
./gererate.sh > Release

Contents文件

dists/$DIST/$COMP/Contents-amd64.gz的文件用来存储文件和软件包的对应关系,这个文件不是必须的。内容如图:

翻译目录

dists/$DIST/$COMP/i18n/目录下存储翻译文件,包括一个Index文件和各语言的翻译,这些文件不是必须的。

Index文件由各个翻译文件的SHA1组成:

各个翻译文件如Translation-zh_CN格式如下所示:

为软件仓库签名

为软件仓库签名就是对Release文件进行签名,并生成InRelease或者Release.gpg文件。

为什么只需要对Release文件签名呢?你也许已经想到了,这是因为信任关系。每个所需软件包的校验值由Packages或Packages.gz文件提供,每个Packages文件的校验值由Release提供,那Release的校验由什么提供呢?由OpenGPG提供。

软件仓库提供者首先需要生成一对OpenGPG密钥,然后使用私钥对Release文件签名,同时在网上发布公钥。在用户端,只有拿到了正确的公钥,才能解密InRelease文件。这个过程保证了所下载的Release文件的正确性,信任链才能成立。所以,用户需要先导入软件仓库提供者的公钥才能正确安全地使用软件仓库。

所以作为软件仓库提供者,我们首先需要一对密钥。

生成OpenPGP密钥对

# 为了方便管理,可以先设置一下GPG工作目录
export GNUPGHOME=/path/to/somewhere/safe
# 生成密钥对
gpg --full-generate-key
# 按照提示输入相关信息即可,建议使用RSA4096,可以兼容GitHub提交密钥

密钥生成后,可以列出已经生成的密钥:

gpg --list-keys

为了方便发布密钥,将公钥导出:

gpg --output pubkey.gpg --export ABCDEFG

若想通过密钥服务器发布,或者兼容旧系统,可以导出ASCII编码的公钥:

gpg --output pubkey.asc --armor --export ABCDEFG

随后你可以选择在OpenPGP.org等服务器发布你的密钥,当然也可以选择不发布,而是在网页上公布。毕竟一旦在密钥服务器上发布,就很难撤回。

注意:私钥一定要保管好,且一定不要误将私钥发布或公布出去了!

签名Release文件

生成加密的InRelease文件:

cat repo-root/my-lfs-os/dists/my-release-v1.1/Release | gpg --default-key ABCDEFG -abs --clearsign > repo-root/my-lfs-os/dists/my-release-v1.1/InRelease

为旧设备生成Releases.gpg:

cat repo-root/my-lfs-os/dists/my-release-v1.1/Release | gpg --default-key ABCDEFG -abs > repo-root/my-lfs-os/dists/my-release-v1.1/Release.gpg

测试软件仓库

使用Nginx或其他Web服务器发布你的软件仓库,确认可以打开:

随后在客户端导入公钥。

导入公钥

如果你像我这样直接在仓库公布密钥,执行以下命令即可导入:

wget -O - http://10.1.1.9/repo-ubuntu-test/pubkey.gpg | sudo tee /usr/share/keyrings/mytest-archive-keyring.gpg

如发布的是ASCII编码的密钥,则首先需要转换成原始文件:

wget -O - http://10.1.1.9/repo-ubuntu-test/pubkey.asc | gpg --dearmor | sudo tee /usr/share/keyrings/mytest-archive-keyring.gpg

对于Debian 11、Ubuntu 22.04及之前的系统,可以直接使用apt-key添加密钥(不推荐,因为添加的密钥会被无条件信任,使得第三方仓库可能替代官方软件仓库):

curl http://10.1.1.9/repo-ubuntu-test/pubkey.asc | sudo apt-key add -

而如果你使用OpenGPG服务器发布密钥,使用以下命令导入密钥:

sudo gpg --no-default-keyring --keyring /usr/share/keyrings/mytest-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys ABCDEFG

添加仓库

编辑/etc/apt/sources.list.d/mytest.list

deb [arch=amd64 signed-by=/usr/share/keyrings/mytest-archive-keyring.gpg] http://10.1.1.9/repo-ubuntu-test bionic main

配置成无条件信任第三方仓库的密钥的话,可以把上面方括号及其内部内容删掉。

运行apt update,若无报错,即说明仓库搭建成功:

apt更新成功

如果密钥有问题,或者没有导入成功,可能会出现这个问题:

密钥检查失败

发表评论