rpm 文件格式
rpm 签名
生成 rpm 包签名的代码流程如下(以 tag rpm-4.14.2.1-release 为例):
// rpmsign.c
doSign
rpmPkgSign
// 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 命令生成的:
$ 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 为例):
// g10/sign.c
sign_file
write_signature_packets
if (timestamp)
sig->timestamp = timestamp;
else
sig->timestamp = make_timestamp();
注意其中对 timestamp
的处理,默认使用当前时间,如果需要修改这个时间戳,可以通过参数传递进来:
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
选项:
$ ./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
#!/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
#!/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
#!/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 之间的转换
$ 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
A pure python rpm reader
Read rpm archive files
https://github.com/srossross/rpmfile
Golang implementation of parsing RPM packages
https://github.com/sassoftware/go-rpmutils
最后修改于 2019-09-25