rpm 签名

生成 rpm 包签名的代码流程如下(以 tag rpm-4.14.2.1-release 为例):

1
2
3
// rpmsign.c
doSign
rpmPkgSign
1
2
3
4
5
6
7
8
9
10
11
12
13
// sign/rpmgensig.c
rpmPkgSign
rpmPushMacro("_gpg_digest_algo")
rpmPushMacro("_gpg_name")
rpmSign
replaceSignature
makeGPGSignature
runGPG
rpmPushMacro("__plaintext_filename");
rpmPushMacro("__signature_filename");
rpmExpand("%{?__gpg_sign_cmd}")
execve
makeSigTag

展开 __gpg_sign_cmd 宏如下,显然 rpm 签名是通过调用 gpg 命令生成的:

1
2
3
4
5
6
7
$ rpm --eval %{?__gpg_sign_cmd}
/usr/bin/gpg
gpg --no-verbose --no-armor

--no-secmem-warning

-u "%{_gpg_name}" -sbo %{__signature_filename} %{__plaintext_filename}

gpg 签名时间戳

gpg 生成签名的流程如下(以 gnupg 2.2.8 为例):

1
2
3
4
5
6
7
// g10/sign.c
sign_file
write_signature_packets
if (timestamp)
sig->timestamp = timestamp;
else
sig->timestamp = make_timestamp();

注意其中对 timestamp 的处理,默认使用当前时间,如果需要修改这个时间戳,可以通过参数传递进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
diff -Nur gnupg-2.2.4.orig/g10/gpg.c gnupg-2.2.4/g10/gpg.c
--- gnupg-2.2.4.orig/g10/gpg.c 2017-08-28 18:22:54.000000000 +0800
+++ gnupg-2.2.4/g10/gpg.c 2019-09-26 10:13:28.207439100 +0800
@@ -345,6 +345,7 @@
oFastListMode,
oListOnly,
oIgnoreTimeConflict,
+ oTimestamp,
oIgnoreValidFrom,
oIgnoreCrcError,
oIgnoreMDCError,
@@ -803,6 +804,7 @@
ARGPARSE_s_n (oPrintPKARecords, "print-pka-records", "@"),
ARGPARSE_s_n (oPrintDANERecords, "print-dane-records", "@"),
ARGPARSE_s_n (oIgnoreTimeConflict, "ignore-time-conflict", "@"),
+ ARGPARSE_s_i (oTimestamp, "timestamp", "@"),
ARGPARSE_s_n (oIgnoreValidFrom, "ignore-valid-from", "@"),
ARGPARSE_s_n (oIgnoreCrcError, "ignore-crc-error", "@"),
ARGPARSE_s_n (oIgnoreMDCError, "ignore-mdc-error", "@"),
@@ -3332,6 +3334,9 @@
case oPrintDANERecords: print_dane_records = 1; break;
case oListOnly: opt.list_only=1; break;
case oIgnoreTimeConflict: opt.ignore_time_conflict = 1; break;
+ case oTimestamp:
+ opt.timestamp = pargs.r.ret_int;
+ break;
case oIgnoreValidFrom: opt.ignore_valid_from = 1; break;
case oIgnoreCrcError: opt.ignore_crc_error = 1; break;
case oIgnoreMDCError: opt.ignore_mdc_error = 1; break;
diff -Nur gnupg-2.2.4.orig/g10/options.h gnupg-2.2.4/g10/options.h
--- gnupg-2.2.4.orig/g10/options.h 2017-08-28 18:22:54.000000000 +0800
+++ gnupg-2.2.4/g10/options.h 2019-09-26 10:13:43.199438957 +0800
@@ -199,6 +199,7 @@
int fast_list_mode;
int legacy_list_mode;
int ignore_time_conflict;
+ u32 timestamp;
int ignore_valid_from;
int ignore_crc_error;
int ignore_mdc_error;
diff -Nur gnupg-2.2.4.orig/g10/sign.c gnupg-2.2.4/g10/sign.c
--- gnupg-2.2.4.orig/g10/sign.c 2017-08-28 18:22:54.000000000 +0800
+++ gnupg-2.2.4/g10/sign.c 2019-09-26 10:15:08.000000000 +0800
@@ -1103,7 +1103,7 @@
/* write the signatures */
rc = write_signature_packets (ctrl, sk_list, out, mfx.md,
opt.textmode && !outfile? 0x01 : 0x00,
- 0, duration, detached ? 'D':'S', NULL);
+ opt.timestamp, duration, detached ? 'D':'S', NULL);
if( rc )
goto leave;

不过需要注意的是,如果指定的时间戳是一个过去的时间,需要指定 --ignore-time-conflict 选项:

1
2
3
4
5
$ ./gpg -sbo x --timestamp 1546275661 misc.c
gpg: key DA3BFDCF8D77F675 was created 8864814 seconds in the future (time warp or clock problem)
gpg: signing failed: Time conflict
$ ./gpg -sbo x --ignore-time-conflict --timestamp 1546275661 misc.c
gpg: key DA3BFDCF8D77F675 was created 8864814 seconds in the future (time warp or clock problem)

改写 rpm

rpm-autosign.exp

1
2
3
4
5
6
7
#!/usr/bin/expect -f

set timeout -1
spawn env LANG=C LC_ALL=C rpm --addsign {*}$argv
expect -exact "Enter pass phrase: "
send -- "\r"
expect eof

rewrite-rpm.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env python

import io
import struct


class RPMError(Exception):
pass


RPM_LEAD_MAGIC = '\xed\xab\xee\xdb'
RPM_HEADER_MAGIC = '\x8e\xad\xe8'


RPMTAG_BUILDTIME = 1006
RPMTAG_SIZE = 1009


def search_magic(rpm, magic):
pos = rpm.tell()
while True:
chunk = rpm.read(len(magic))
if not chunk or len(chunk) != len(magic):
return None
if chunk == magic:
return pos
pos += 1
rpm.seek(pos)


def _read_signature(rpm):
# magic
pos = search_magic(rpm, RPM_HEADER_MAGIC)
if pos is None:
raise RPMError('invalid RPM file, signature header not found')

# header version
_ = ord(rpm.read(1))
# reserved
_ = rpm.read(4)

# index entry count
num_entries, = struct.unpack(b'!i', rpm.read(4))
# store size
store_size, = struct.unpack(b'!i', rpm.read(4))

# index entry: tag, type, offset, count
entry_fmt = struct.Struct(b'!iiii')
for _ in range(num_entries):
# entry
_ = rpm.read(entry_fmt.size)

# store
_ = rpm.read(store_size)


def _update_rpm(rpm, tags):
# magic
pos = search_magic(rpm, RPM_HEADER_MAGIC)
if pos is None:
raise RPMError('invalid RPM file, rpm header not found')

# header version
_ = ord(rpm.read(1))
# reserved
_ = rpm.read(4)

# index entry count
num_entries, = struct.unpack('!i', rpm.read(4))
# store size
store_size, = struct.unpack('!i', rpm.read(4))

# entry: tag, type, offset, count
entry_fmt = struct.Struct('!iiii')
entries = []
for _ in range(num_entries):
entry = entry_fmt.unpack(rpm.read(entry_fmt.size))
entries.append(entry)

pos = rpm.tell()
store = io.BytesIO(rpm.read(store_size))

for tag, typ, offset, count in entries:
d = tags.get(tag)
if d is None:
continue

# rewrite entry data
hd = struct.pack('!i', d)
store.seek(offset)
store.write(hd)

store.seek(0)

rpm.seek(pos)
rpm.write(store.read())


def update_rpm(rpm, tags):
lead_fmt = struct.Struct(b'!4sBBhh66shh16s')
data = rpm.read(lead_fmt.size)
lead = lead_fmt.unpack(data)

magic = lead[0]
if magic != RPM_LEAD_MAGIC:
raise RPMError('invalid RPM file')

# signature
_read_signature(rpm)

# the real data
_update_rpm(rpm, tags)


def parse_args():
import argparse

parser = argparse.ArgumentParser()
parser.add_argument(
'--build-time',
type=int,
metavar='BUILDTIME',
help='rpm package build time'
)
parser.add_argument(
'--size',
type=int,
metavar='SIZE',
help='rpm package size'
)
parser.add_argument(
'rpm',
type=str,
help='rpm package to rewrite'
)

return parser.parse_args()


def main():
args = parse_args()

tags = {
RPMTAG_BUILDTIME: args.build_time,
RPMTAG_SIZE: args.size
}

with open(args.rpm, 'r+b') as rpm:
update_rpm(rpm, tags)


if __name__ == '__main__':
main()

rewrite-rpm.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/bin/bash

SCRIPT_DIR=$(cd -P $(dirname $0) && pwd -P)

KEYID=C7E8A950

setup_gpg() {
export GNUPGHOME=${SCRIPT_DIR}/gpg

if ! gpg --list-keys 2>&1 | grep ${KEYID} > /dev/null; then
echo "Can not find signing key" 1>&2
return 1
fi
}

sign_rpm() {
rpm="$1"
timestamp=$2
keyid=$3

# /usr/lib/rpm/macros
gpg_cmd='%{__gpg} \
gpg --timestamp %{_gpg_timestamp} --ignore-time-conflict --no-verbose --no-armor \
%{?_gpg_digest_algo:--digest-algo %{_gpg_digest_algo}} \
--no-secmem-warning \
%{?_gpg_sign_cmd_extra_args:%{_gpg_sign_cmd_extra_args}} \
-u "%{_gpg_name}" -sbo %{__signature_filename} %{__plaintext_filename}'

rpm --delsign ${rpm}
${SCRIPT_DIR}/rpm-autosign.exp \
--define "_gpg_timestamp ${timestamp}" \
--define "_gpg_name ${keyid}" \
--define "__gpg_sign_cmd ${gpg_cmd}" \
${rpm}
}

update_rpm() {
ref_rpm="$1"
rpm="$2"
keyid=$3

build_time=$(rpm -qp --queryformat '%{BUILDTIME}' --nodigest --nosignature ${ref_rpm})
size=$(rpm -qp --queryformat '%{SIZE}' --nodigest --nosignature ${ref_rpm})
signature_time=$(rpm -qpi --nodigest --nosignature ${ref_rpm} | awk 'BEGIN {FS = ","}; /Signature\s*:/ {print $2}' | xargs -I{} date -d {} +%s)

${SCRIPT_DIR}/rewrite-rpm.py --build-time ${build_time} --size ${size} ${rpm}

sign_rpm ${rpm} ${signature_time} ${keyid}
}

setup_gpg
if [ $? -ne 0 ]; then
exit 1
fi

# $1 is ref rpm, $2 is rpm to update
update_rpm "$1" "$2" ${KEYID}

# example:
# cd /new-packages-dir/
# for i in $(ls /old-packages-dir/*.rpm); do name=$(basename $i); if [ -f $name ]; then /path/to/rewrite-rpm.sh $i $name; fi; done

时间与 epoch 之间的转换

1
2
3
4
5
6
7
8
$ date -d '2019/01/02 11:11:11' +%s
1546398671
$ date -d @1546398671
Wed Jan 2 11:11:11 CST 2019
$ date -d @1546398671 +%s
1546398671
$ date -d 'Mon 23 Sep 2019 03:49:44 PM CST' +%s
1569224984

参考资料

RPM File Format

http://ftp.rpm.org/max-rpm/s1-rpm-file-format-rpm-file-format.html

Description of RPM file format

https://rpm.org/devel_doc/file_format.html

RPM Package File Structure

https://docs.fedoraproject.org/en-US/Fedora_Draft_Documentation/0.1/html/RPM_Guide/ch-package-structure.html

A pure python rpm reader

https://github.com/mjvm/pyrpm

Read rpm archive files

https://github.com/srossross/rpmfile

Golang implementation of parsing RPM packages

https://github.com/sassoftware/go-rpmutils