runsisi's

technical notes

rpm 文件格式

2019-09-25 runsisi#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

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