The aim of this post is showing a tidy way to structure a C o C++ project managing the build lifecycle using the GNU Make and packaging it as RPM.

The post demonstrates a full featured C project managed by make and packaged as RPM, showing how to set up a tidy structure, develop and package a C application with its own shared objects, that reads the configuration from a file, validates settings, logs events into a file and handles error conditions printing to standard error and setting properly shell return code.

This post is certainly useful not only to developers, but to anybody who wants to learn how to build third part C or C++ software, since it clearly describes the compilation and linking process. In addition to that, we also learn how to create the product certificate that  can be exploited by the subscription-manager to know that the product is installed on the system.

The application is then packaged, besides as a gzipped tarball, also as RPM, creating the application package, the package with the development resource files (the C include files) and the package with the debug information that can be used with a debugger to troubleshoot things.

This post is focused on the C programming language, but the very most of the concepts related to  the build life-cycle managed with GNU Make shown apply to C++ too: I chose C only to show a way of doing things that works also with a legacy (but yet powerful) programming language. In addition to that, be wary that I'm striving to cover most of the scenarios: this means that I'm showing things that are not always necessary in every use case.

When I decided to write this post I was a little bit nostalgic, ... I developed a lot using the C programming language (I mostly used C++, honestly) when I was a little bit more than a teenager, ... this reminds me of the last years at the high school and the first years at university.

I was really impressed by what Dennis Ritchie and Bjarne Stroustrup did. Even nowadays we find a lot of emblazoned applications that have been developed using C and C++ - despite nowadays I firmly believe that Golang is the new way forward.

I chose to show how to set up a project using the programming language created by Dennis Ritchie, to celebrate a programming language that after 50 years is still here, so I think that it is kind to claim:

In memory of Dennis Ritchie, father of the C programming language and key developer of the UNIX.

Improving The Software Maintainability And The Easiness Of Delivery And Installation

There are several things to consider when planning the structure of a program - I won't tell you about the best practices about the code itself - this post is not about the best practices for your C code: instead this post focuses on highlighting the best practices, broadly used since many years, that helps to ease the software maintainability and delivery, making it more friendly to install, maintain and and support.

Provide The Software Development Files

Unless you are working on a trivial application, it is quite likely that some advanced users may want to write integrations to it, and so you must find a convenient way to deliver the Software Development Kit (SDK) with all the resource files necessary to develop integrations with the application.

Note that when dealing with C or C++ applications, this does not mean that you must provide the full source code of the application: providing headers files and shared libraries is enough to let other people be able to write their own code that easily links to yours.

Anyway mind that delivering the full sources of the application is always interpreted as a signal of transparency.

Provide The Help Command Switch And Man Pages

Your application certainly has command line utilities with options, ... of course an help command switch is handy (such has -h or --help), but a man page provides much more space to better explain concepts, and on UNIX and Linux we are all used to lookup man pages of command line tools.

So do not forget to provide man pages: they are the most handy way to get quick help.

Provide Documentation

Of course you are then expected to integrate them with more modern documentation that handles also graphical and or multimedia contents, such as HTML or PDF, ... your users are certainly glad to have documentation files provided along with the application.

Be aware Of Your Target  Platforms

Professionally providing an application is much more than simply creating a package and the deliverying it: the application and the installation package must be designed so as to integrate at its best with the target platform.

For example, Linux has a standard filesystem layout: a well designed application must be compatible with the standard Linux filesystem layout and exploit it, allowing to place configuration and log files in the most convenient path.

But at the same time you must provide a way to install things within a directory tree beneath a common root, so that unprivileged users (such as the developers themselves) can install it for example for testing.

Exploit The Package Manager Specific Of The Platform

Package the application using a format that is supported by the native Package Manager of the platform - this provides several benefits, such as:

  • easily install the application
  • quickly identify installed files and version
  • easily upgrade the application
  • easily uninstall the application

The Development Environment

A development environment for C (or C++) requires just a few tools:

  • a C (or C++) compiler
  • the C libraries for the architecture in use
  • a project handling utility that helps us to work when dealing with hundreds, if not thousands of files.

Install The C Compiler And The Make Utility

So the utility we need to install are:

  • gcc - the C compiler and linker
  • make - the management system of the project

Let's install the development tools as follows:

sudo dnf install -y gcc make
Please mind that if you want to develop with C++ you must install the g++ RPM package.

Install RPM Build Tools

We also need the RPM development tools for packaging the application files and sources: for example RPMs are built using the rpmbuild command line utility indeed.

We can install them along with other handy tools such as rpmdevtools and rpmlint as follows:

sudo dnf install -y rpm-build rpmdevtools rpmlint

Working as A Regular User

In this post we work as the "student" user, so login as the user you want to use for developing or switch to it using sudo:

sudo su - student
Mind that we are acting as a developer, so we must use a regular user: developing as a user with administrative rights is a very dangerous habit that you must avoid, since there's a high risk of breaking the operating system if something does not work as you expect.

A good way of working is having a directory to store every developed project into: in this post this is the "devel" directory: create it as follows:

mkdir -m 755 ~/devel

Creating the project folder tree

Let's change to the "devel" directory and create the root directory of the project for the "foo" application we want to develop and package:

cd ~/devel
mkdir -m 755 foo

and of course change to this directory:

cd ~/devel/foo

we are now in the root directory of the project of the "foo" application: here we must create the directory tree with a layout that keeps things tidy and easy to find, so to ease the maintenance of the project.
Let's begin by creating the directories where we organize files:

mkdir -m 755 src etc bin lib log man doc cert RPM

the purpose of each directory is:

  • src - the source files of the program
  • bin - store the binary files generated by the build
  • lib - store the shared object files generated by the build
  • etc - store configuration files
  • log - store log files written by the application while running
  • man - store man files
  • doc - store documentation files, such as README files. HTML, MD files, PDF and so on
  • cert - store the product certificate
  • RPM - store the RPM spec file used to build the RPM packages that contain the application and its sources

Developing the application

Let's begin developing the program - change to the "src" directory:

cd src

programs developed using the C programming language are made of two kind of files:

  • header files, that contain only the headers of the functions, without implement anything
  • C code files, that contain the code necessary to implement the functions declared in the headers

The outcome of the building of these files are the object files. Programs are then assembled by the linker by linking together all the object files, creating a monolithic executable file.

An alternate more handy way is using shared object files (*.so): when exploiting this feature the linker assembles the executable, but delays the actual loading of the code contained in the object file until the time of the actual run, when it actually loads them from the linked shared libraries (the .so files).

It is up to the developer to decide which object files link together with the main application, and which to put inside shared libraries. The rule of thumb is that object files that refer to headers packaged as SDK files must be put in shared libraries, so that they can be linked and loaded by newly developed applications.

Header files are used by the compiler to check the semantic of the call to functions, so they must be detached from the main RPM package, and put into a "devel" package.

When dealing with large  C projects, there are often hundreds and even thousands of headers and C code files: to enforce tidiness, files are often logically grouped by purpose into directories.

Mind that ".h" files are not used only to list function prototypes: they have also other purposes, such as defining global variables, data structure or symbols. Auto-tools for example creates a "config.h" file containing the outcome of the system scan, such as the architecture family and many other symbols that are then exploited to perform a fine grained build of the application. I will not say anything about auto-tools in this post, since it is a huge topic that deserves a whole post at least.

By the way it seems that I forgot to mention the purpose of this program, ... that's very Agile ;O) . So don't worry about the features, we must welcome them later on.

Anyway, any good program must at least:

  • load its configuration from a file
  • log its status messages into a file

We can group these two features in a common feature called "facilities".

Implementing the facilities feature

Let's create the "facilities" directory within the "src" directory:

mkdir -m 755 facilities

change to the "facilities" directory:

cd facilities

At first let's create the "logging.h" header file where we declare functions that implements the logging handling feature:

#ifndef LOGGING_H_
  #define LOGGING_H_

#define LOGGING_FILE "../log/foo.log"

#include <stdio.h>

FILE *openlog(void);
int logmessage(FILE *fp, char *message);
void closelog(FILE *fp);

#endif

then let's create the "logging.c" file with the code that implements it:

#include "logging.h"
#include <time.h>

FILE *openlog(void) {
    FILE *fp;
    fp = fopen(LOGGING_FILE, "a");
    if (fp == NULL) fprintf( stderr, "Unable to open \"%s\" log file\n", LOGGING_FILE);
    return fp;
}

int logmessage(FILE *fp, char *message){
    time_t timer;
    char timestamp[26];
    struct tm* tm_info;

    timer = time(NULL);
    tm_info = localtime(&timer);

    strftime(timestamp, 26, "%Y-%m-%d %H:%M:%S", tm_info);
    return fprintf(fp, "%s - %s", timestamp, message);
}

void closelog(FILE *fp){
    fclose(fp);
}

same way, lets create the "config.h" header file where we declare functions that implements the configuration handling feature:

#ifndef CONFIG_H_
  #define CONFIG_H_

#define CONFIG_FILE "../etc/foo.conf"
#include <stdio.h>

struct settings {
  char behavior[255];
};

int loadconfig(struct settings *config, FILE *logfp);

#endif

and eventually we create the "config.c" file with the code that implements it:

#include <stdlib.h>
#include <string.h>
#include "config.h"
#include "logging.h"

int loadconfig(struct settings *config, FILE  *logfp) {
    FILE * fp;
    char * line = NULL;
    char *key;
    char *value;
    size_t len = 0;
    ssize_t read;
    int status = 0;

    fp = fopen(CONFIG_FILE, "r");
    if (fp == NULL) {
        fprintf( stderr, "Unable to open \"%s\" file\n", CONFIG_FILE);
        return -1;
    }
    while ( (read=getline(&line, &len, fp) ) != -1 ) {
        key=strtok(line, "=");
        value=strtok(NULL, "\n");
        if(strcmp(key, "behavior")==0) {
           if (strcmp(value,"kind") !=0 && strcmp(value,"aggressive")!=0) {
             fprintf( stderr, "invalid value \"%s\" for setting behavior: can be either \"kind\" or \"aggressive\"\n", value);
             logmessage(logfp, "invalid value for setting behavior: can be either \"kind\" or \"aggressive\"\n");
             status=-1;
           }
           strncpy(config->behavior, value, 255);
        }
    }
    fclose(fp);
    if (line) free(line);
    return status; 
}

please note that the code in this file requires also the code of the logging feature - you can guess it from the #include "logging.h" directive.

We must now create the "Makefile" file that governs the build lifecycle of the files in this directory ("src/facilities"):

all: foo

foo:
	gcc -Wall -g -c -fPIC config.c
	gcc -Wall -g -c -fPIC logging.c
	gcc -shared config.o logging.o -o ../../lib/libfacilities.so

clean:
	rm -f *.o

install:
	install -m 0755 -d $(ROOT)$(INCLUDEDIR)/facilities
	install -m 0755 *.h $(ROOT)$(INCLUDEDIR)/facilities

dist: clean
Please note that we compile using the -g option (lines 4-5): this causes the compiler to add debugging information to the generated files - by default it uses the DWARF format, although older formats, such as STABS do exist. Debugging information is used by debuggers to relate object code data to the lines of the source code. The drawback is a "fat" binary file. Anyway, as we'll see later on, RPM packaging strips them from the executable and puts them in separate sub-packages that you can install if you need to debug the application.
The compiler also has the -0g option that provides an improved debugging experience. Anyway do not build applications with these options: it disables the optimization, so the application becomes highly inefficient. This option must be used only to build an additional build of the application specifically dedicated to debugging that must be delivered only when needed.

as you see we define the following targets:

  • foo: compiles the sources generating object files (-c option) with Position-Independent Code (fPIC option) - this particular kind of code is necessary since it puts them into the libfacilities.so shared object beneath the "lib" directory of the project ("~/devel/foo/lib")
  • clean: deletes the built object files - that is cleanup the workspace
  • install: install the header files into the desired path
  • dist: an empty target that launches the "clean" target. it is unused here, but is required by the master Makefile we will develop more over.
Be wary that shared library file names must have the "lib" prefix.

If you are curious to see things in action, we can already have a go as follows:

make

The outcome is the libfacilities.so file into the "lib" directory of the project - as you can easily guess, this is a shared library.

We can of course cleanup the directory from the built objects by running "make clean", or otherwise install the header files as follows:

export INCLUDEDIR=~/foo/include
make install

by exporting the INCLUDEDIR environment variable before running the command, "logging.h" and "config.h" files get installed beneath the "~/foo/include" directory.

Note that the make install governed by this make file just installs the header files: since the shared objects are created in another directory - the "lib" directory of the project, we must create a Makefile that manages its contents in the "lib" directory itself.

Change directory to the "lib" directory:

cd ~/devel/foo/lib

and create the Makefile file with the following contents:

clean:
	rm -f *.so

install:
	install -m 0755 -d $(ROOT)$(LIBDIR)
	install -m 0755 *.so $(ROOT)$(LIBDIR)

dist:

we can see how the install target of the Makefile does work as follows:

export LIBDIR=~/foo/lib
make install
Note that the "install" target has been made so to install every shared object file found (*.so) to the LIBDIR directory: this way you do not have to worry to modify it as new shared libraries are developed.

There is of course a cleanup target too - let's try it:

make clean

it cleans up the directory from every shared object file found.

Implementing the greet feature

I think that I cannot delay declaring the purpose of the "foo" application more, and so I hereby claim that

As a user I want a command that greets every time it is run. And I want it configurable so to be either kind or aggressive too.

So we must implement the greet feature: let's create the "greet" directory within the "src" directory:

cd ~/devel/foo/src
mkdir -m 755 greet

change to the "greet" directory:

cd greet

at first create the "greet.h" header file where we declare functions that implements the greet feature:

#ifndef GREET_H_
  #define GREET_H_

void greet(struct settings *config, FILE *logfp);

#endif

then create the "greet.c" file with the code that implements it:

#include "../facilities/config.h"
#include "../facilities/logging.h"
#include <string.h>

void greet(struct settings *config, FILE *logfp){
    if(strcmp(config->behavior,"kind")==0) {
        printf("Hi dear, I'm foo app, glad to see you\n");
        logmessage(logfp, "kindly greeting\n");
    }
    if(strcmp(config->behavior,"aggressive")==0) {
        printf("I'm foo app, I already told you not to disturb me!\n");
        logmessage(logfp, "aggressive greeting\n");
    }
}

lastly we must create the Makefile that governs the build lifecycle of the files in this directory ("src/greet"):

all: foo

foo:
	gcc -Wall -g -c -fPIC greet.c
	gcc -shared greet.o -o ../../lib/libgreet.so

clean:
	rm -f *.o

install:
	install -m 0755 -d $(ROOT)$(INCLUDEDIR)/greet
	install -m 0755 *.h $(ROOT)$(INCLUDEDIR)/greet

dist: clean

as you see we define the following targets:

  • foo: compiles the sources generating object files (-c option) with Position-Independent Code (fPIC option) - this particular kind of code is necessary since it puts them into the libgreet.so shared object beneath the "lib" directory of the project ("~/devel/foo/lib")
  • clean: deletes the built object files - that is cleanup the workspace
  • install: install the header files into the desired path
  • dist: an empty target that launches the "clean" target. it is unused here, but is required by the master Makefile we will develop more over.

same as we did before, if you are curious, we can now have a go as follows:

make

the outcome is the libgreet.so file into the "lib" directory of the project.

We can of course cleanup the directory from the built objects by running "make clean", otherwise install the header files as follows:

export INCLUDEDIR=~/foo/include
make install

by exporting the INCLUDEDIR environment variable before running the command, the "greet.h" header file gets installed beneath the "~/foo/include" directory.

Implementing the main object

It has now come the time to implement the main object of the foo application.

Change back to the parent directory ("src"):

cd ~/devel/foo/src

and create the "main.c" file with the following contents:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>

#include "facilities/config.h"
#include "facilities/logging.h"
#include "greet/greet.h"

int main (int argc, char *argv[]) {
    struct settings config;
    FILE *log;
    log=openlog();
    if(log==NULL) exit(EXIT_FAILURE);
    if(loadconfig(&config,log)!=0) exit(EXIT_FAILURE);
    greet(&config,log);
    closelog(log);
    exit(EXIT_SUCCESS);
}

we of course still need create the Makefile that assembles the main application: create it with the following contents:

all: foo

foo:
	$(MAKE) -C greet $(MAKECMDGOALS)
	$(MAKE) -C facilities $(MAKECMDGOALS)
	gcc -Wall -g -c main.c
	gcc main.o -o ../bin/foo -L../lib -lgreet -lfacilities

clean:
	$(MAKE) -C greet $(MAKECMDGOALS)
	$(MAKE) -C facilities $(MAKECMDGOALS)
	rm -f *.o

install:
	$(MAKE) -C greet $(MAKECMDGOALS)
	$(MAKE) -C facilities $(MAKECMDGOALS)

dist: clean

as you can see  "foo" is the default target that:

  • enters the "greet" and "facilities" directories and launches the nested Makefiles (lines 4-5)
  • compiles the main program generating the "main.o" object adding debugging information (line 6)
  • assembles the "main.o" object linking it with the "libgreet.so" and "libfacilities.so" shared libraries into the "foo" ELF binary file (line 7)
If you are curious to see how do debug information look like, you can display DWARF information stored within an ELF file by running the "readelf -w file" command, where the file can be either a binary ELF, an object file (*.o) or a shared library (*.so).

we are ready to build the whole "foo" application:

make

the "foo" executable binary file is put beneath the "bin" directory of the project; lets change to that directory:

cd ~/devel/foo/bin

and create the Makefile that manages its installation:

clean:
	rm -f foo 

install:
	install -m 0755 -d $(ROOT)$(PREFIX)/bin
	install -m 0755 foo $(ROOT)$(PREFIX)/bin

dist:

before trying to install it using the make install command, let's launch the foo application, just to see if it can even work:

./foo

the outcome may disappoint your expectations:

./foo: error while loading shared libraries: libgreet.so: cannot open shared object file: No such file or directory

this is not an error - it is simply complaining that we tried to launch the application without properly setting up the environment: foo has been linked to shared libraries, but they are not in the standard path indeed.

Shared libraries are sought beneath the path specified by the LD_LIBRARY_PATH environment variable.

This means that we must set the LD_LIBRARY_PATH environment variable to the directory containing the "libgreet.so" and "libfacilities.so" files:

LD_LIBRARY_PATH=~/devel/foo/lib ./foo

it is still complaining, ... but this time for an error handled by the application:

Unable to open "../etc/foo.conf" file

so it is straightforward that we must create the "../etc/foo.conf" configuration file. Create it with the following contents:

behavior=kind

let's try to have a go again now:

LD_LIBRARY_PATH=~/devel/foo/lib ./foo

and this time it is magically working!

Hi dear, I'm foo app, glad to see you

so it is evident that the "etc" directory and the "etc/foo.conf" file are required to run the foo application. This is something those who install the foo application must be wary about.

But since we are kind, we can automate the installation for them by creating the Makefile that manages their installation right into each directory.

So let's create the Makefile beneath the "etc" directory:

install:
	install -m 0755 -d $(ROOT)$(CONFDIR)
	install -m 0644 foo.conf $(ROOT)$(CONFDIR)

be wary that the "foo" application also writes log messages to a file: in order to do a good job we must  also create the Makefile that creates the "log" directory or, once installed, the application will not run complaining that it cannot open the log file for writing.

Simply create a Makefile with the following contents in the "log" directory:

install:
	install -m 0755 -d $(ROOT)$(LOGDIR)

as you see the install target takes care of creating the log directory in the path specified by the LOGDIR environment variable.

Creating the Man Pages

Unless you are sadistic and love to see people doing reverse engineering to be able to run the applications you write, you must provide their documentation.

It is straightforward that a user may want the answers to questions like:

what's the file path of the configuration file?

what are the valid values of the "behavior" setting?

and so on.

Times ago it was fashionable to document command line tools creating a man page with the same name of the command itself: I'm a romantic and I fancy this habit.

Since the "foo" application is a command line tool, let's see how to create its man page.

Let's change to the "man" directory:

cd ~/devel/foo/man

Linux man pages are text files processed by the groff text formatter.

Groff is the GNU implementation of troff and nroff, two text processing utilities implemented many, many years ago at Bell Labs. It is an ancient yet powerful tool that is even suitable for creating documents, articles and even books.

Instead of directly writing the man page, we create a template from which to generate the actual man page: this way we can easily replace the placeholders in the template with data such as the release version, so as to produce a man page that reflects the actual release version of the command line it refers to.

Let's create the "foo.template" file with the following contents:

\" Manpage for foo.
.\" Contact me@fake.tld to correct errors or typos.
.TH man 8 "16 Jan 2022" "__VERSION__" "foo man page"
.SH NAME
foo \- greet the user
.SH SYNOPSIS
foo
.SH DESCRIPTION
foo is the ultimate greeting utility.
.SH OPTIONS
foo does not take any options.
.SH FILES
settings can be specified by configuring __CONFIG_DIR__/foo.conf file
.SH SETTINGS
The following settings are implemented:
.IP \fBbehavior\fR
the behavior of greet to issue: valid values are "kind" or "aggressive"
.SH BUGS
No known bugs.
.SH AUTHOR
Marco Antonio Carcano (me@fake.tld)

to automate and ease the generation of the actual man page we rely on a Makefile with the following contents:

foo:
	cp foo.template foo
	sed -i "s/__VERSION__/${VERSION}/g;s+__CONFIG_DIR__+${CONFDIR}+" foo

clean:
	rm -f foo

install:
	install -m 0755 -d $(ROOT)$(MANDIR)
	install -m 0644 foo $(ROOT)$(MANDIR)

dist:

let's try it and generate the foo man page - note that we must set some environment variable before:

VERSION=1.0 CONFDIR=../etc make

the outcome is the creation of the "foo" file - that is the actual man page derived from the template.

Let's have a look to it:

man ./foo

the output is as follows:

man(8)                                    foo man page                                    man(8)

NAME
       foo - greet the user

SYNOPSIS
       foo

DESCRIPTION
       foo is the ultimate greeting utility.

OPTIONS
       foo does not take any options.

FILES
       settings can be specified by configuring ../etc/foo.conf file

SETTINGS
       The following settings are implemented:

       behavior
              the behavior of greet to issue: valid values are "kind" or "aggressive"

BUGS
       No known bugs.

AUTHOR
       Marco Antonio Carcano (me@fake.tld)

1.0                                        16 Jan 2022                                    man(8)

Creating the Documentation

Man pages are certainly handy when working with a command line, but there are of course more modern and attractive documentation formats, such as HTML pages, MarkDown documents or PDF.

These documentation files are historically put beneath the "doc" directory: let's change to this directory:

cd ~/devel/foo/doc

now create the README file for the "foo" application:

This is the amazing foo app that can greet you better than any other application can do.

and of course let's create the Makefile file to install it:

install:
	install -m 0755 -d $(ROOT)$(DOCDIR)
	install -m 0644 README $(ROOT)$(DOCDIR)
	sed -i "s+__CONFIGDIR__+$(CONFDIR)+" $(ROOT)$(DOCDIR)/README

this time we need just the install target.

Generating The Product Certificate

Although this is not mandatory, you can add a product certificate too: this is a certificate with custom metadata that can be exploited by the auto-attach feature of subscription-manager to guess what products are compatible with the installed operating system version, or when providing a criteria to automatically attach the matching products.

The RPM package must install it beneath the "/etc/pki/product" directory, into a PEM file with the numeric ID of the product as name. For example the file with the product certificate of "Red Hat Enterprise Linux" is 69.pem.

A product certificate contains the metadata that is stored using OIDs children of "1.3.6.1.4.1.2312.9.1": the first children number of the namespace is the numeric ID of the product (that marks the beginning of the product namespace).

The straightforward question is, which ID number can I assign to the product? The answer can surprise and disappoint you - you cannot pick up whatever you want - you must first create the product on the Red Hat Network Satellite Server 6 or Katello; the ID is automatically generated by them and you can see it by navigating "Content" / "Subscription", selecting the product: the "Subscription Info" page is shown, where you can get the "Product ID". There's no way to force a product to have a custom ID. The only pity is that you cannot set a number at wish when you create a product, ... it's a big pity. If you would like such a feature, make your voice heard to the Katello project.

For example, Red Hat Enterprise Linux has been assigned to the ID 69, and it's OID namespace is "1.3.6.1.4.1.2312.9.1.69".

Beneath the product namespace there are the following attributes

  • Name: the name of the product, for example "Foo"
  • Version: the version of the product, for example "1.0"
  • Arch: the architecture family of the product, for example "x86_64"
  • Tags: a comma-separated list of tags assigned to the product. These are often used to express affinities. For example "foo1-centos-8,centos-8"
  • Brand Type: an optional attribute with the brand type
  • Brand Name: an optional attribute with the brand name

In this example we suppose that the Product ID of the "Foo" product fetched from our Red Hat Network Satellite Server 6 (or of course Katello) is "656104112056".

Since every custom attribute we are about to add has its own OID, it is straightforward that to generate such a kind of certificate we need a dedicated openssl config file that lists all of them and instructs openssl on how to add them to the Certificate Signing Request (CSR), so that the signing Certification Authority can then copy them to the actual certificate.

As an example, let's create the "products.conf" file with the following contents:

It is straightforward that these are custom OIDs that must be added to the certificate.

So it is necessary to create an openssl configuration file that describes them and how to put them into the certificate signing request.

First we need to chan to the "cert" directory:

cd ~/devel/foo/cert

and create the "product.template" file with the following contents:

id_section = contents_oid

[ contents_oid ]
Name=1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.1
Version=1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.2
Arch=1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.3
Tags=1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.4

[ req ]
distinguished_name  = req_distinguished_name
req_extensions      = product

[ req_distinguished_name ]
commonName                      = Common Name

[ product ]
basicConstraints = CA:FALSE
1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.1 = ASN1:UTF8String:__PRODUCT_NAME__
1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.2 = ASN1:UTF8String:__VERSION__
1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.3 = ASN1:UTF8String:__ARCH__
1.3.6.1.4.1.2312.9.1.__PRODUCT_ID__.4 = ASN1:UTF8String:__TAGS__

we use it as the template from which to derive the "product.conf" openssl config file we use to generate the Certificate Signing Request for the product certificate.

Same way as we are doing with man pages, we exploit the Makefile to generate the "product.conf" file: create the Makefile with the following contents:

foo:
	@cp product.template product.conf
	@sed -i "s/__PRODUCT_NAME__/${PRODUCT_NAME}/g;s/__PRODUCT_ID__/${PRODUCT_ID}/g;s/__ARCH__/${ARCH}/g;s/__VERSION__/${VERSION}/g;s/__TAGS__/${PRODUCT_TAGS}/g" product.conf
	@[ -f product.key ] || openssl genrsa -out product.key 4096
	@openssl req -config product.conf -key product.key -new -sha256 -out ${PRODUCT_ID}.csr -subj '/CN=${PRODUCT_CN}'
	@[ -f ${PRODUCT_ID}.crt ] || echo "################################################################"
	@[ -f ${PRODUCT_ID}.crt ] || echo "Certificate  Signing  Request  completed.  You must now send the"
	@[ -f ${PRODUCT_ID}.crt ] || echo "cert/${PRODUCT_ID}.csr file to your Certification Authority."
	@[ -f ${PRODUCT_ID}.crt ] || echo "the certificate file you get back from them must be renamed into"
	@[ -f ${PRODUCT_ID}.crt ] || echo "${PRODUCT_ID}.crt  and  put   into  the  \"cert\"  directory."
	@[ -f ${PRODUCT_ID}.crt ] || echo "################################################################"

install:
	install -m 0755 -d $(ROOT)$(CERTDIR)
	install -m 0644 ${PRODUCT_ID}.crt $(ROOT)$(CERTDIR)/${PRODUCT_ID}.pem

dist:

as you can see there are two targets: "install" that installs the product certificate, and "foo" (the default target by the way) that generates:

  • the "product.conf" openssl config file
  • the RSA key that is used for the Certificate Signing Request for this product
  • the Certificate Signing Request, that is a ".csr" file with the IP of the product as name

Once generated, the Certificate Signing Request file must be sent to the Corporate's Certification Authority to have then returned the signed X.509 certificate.

Just as an example, if the Corporate's CA relies on openssl and the Product ID is "656104112056", the certificate generation command is:

openssl ca -config openssl.conf -days 7000 -in 656104112056.csr -out 656104112056.crt

the certificate must then be added to the project, so that it is put among the files provided by the RPM package.

The certificate

  • must be renamed so to have the same name of the Certificate Signing Request file, with the ".crt" extension
  • must be put in the "cert" directory
Since the "install" target requires the certificate file to be in the "cert" directory, the next steps won't work until you put the product certificate there..

Don't run it now, but please note that if you want to run this Makefile as we did before you need first to export the following environment variables:

  • PRODUCT_NAME
  • PRODUCT_ID
  • ARCH
  • TAGS

Same way, if you want to  run the "install" target, you must at least export the CERTDIR variable.

Anyway do not launch it now: it will be executed as a whole when running the main Makefile of the project.

If you want to learn more about product certificates, subscription-manager and registering clients to the Red Hat Network Satellite Server 6 or Katello, don't miss the post "Register Clients To Satellite Server 6 Or Katello".

 

Create the Main Makefile Of the Project

You certainly have got that manually running all these Makefiles is not handy at all: indeed the project must be governed by a root Makefile, that when run takes care to launch the relevant nested Makefiles specifying the same target used to run it.

Change to the root directory of the project:

cd ~/devel/foo

and create the Makefile file with the following contents:

.EXPORT_ALL_VARIABLES:
ROOT ?=
VERSION ?= 0.1
PREFIX ?= ~/foo
CONFDIR ?= ${PREFIX}/etc
DOCDIR ?= ${PREFIX}/doc
INCLUDEDIR ?= ${PREFIX}/include
LIBDIR ?= ${PREFIX}/lib
LOGDIR ?= ${PREFIX}/log
MANDIR ?= ${PREFIX}/man
CERTDIR ?= ${PREFIX}/product
COMPANY_NAME ?= "Carcano SA"
PRODUCT_UID ?= "1fac-31230-aafe-1111"
PRODUCT_CN="${COMPANY_NAME} Product ID [${PRODUCT_UID}]"
PRODUCT_NAME ?= "Foo"
PRODUCT_ID ?= "656104112056"
PRODUCT_TAGS ?= "foo-1-centos8,centos-8"
ARCH ?= $$(uname -i)

all: foo

foo:
	$(MAKE) -C src $(MAKECMDGOALS)
	$(MAKE) -C man $(MAKECMDGOALS)
	cd cert; make $(MAKECMDGOALS); cd ..

clean:
	$(MAKE) -C src $(MAKECMDGOALS)
	$(MAKE) -C bin $(MAKECMDGOALS)
	$(MAKE) -C lib $(MAKECMDGOALS)
	$(MAKE) -C man $(MAKECMDGOALS)
	rm -f foo-${VERSION}.tar.gz

install:
	$(MAKE) -C src $(MAKECMDGOALS)
	$(MAKE) -C bin $(MAKECMDGOALS)
	$(MAKE) -C lib $(MAKECMDGOALS)
	$(MAKE) -C etc $(MAKECMDGOALS)
	$(MAKE) -C doc $(MAKECMDGOALS)
	$(MAKE) -C man $(MAKECMDGOALS)
	$(MAKE) -C log $(MAKECMDGOALS)
	$(MAKE) -C cert $(MAKECMDGOALS)

dist: clean
	@rm -f log/foo.log
	@tar czf foo-${VERSION}.tar.gz \
                --transform "s,^,foo-${VERSION}/," \
                src etc lib/Makefile bin/Makefile \
                log doc man cert Makefile

this Makefile governs the whole project:

it declares all the targets required to manage the build cycle of the project:

  • foo: build the whole application (the executable, the shared objects and the documentation) and generates the CSR of the product certificate
  • install: install the application along with its config file and shared objects, the documentation and the header files necessary to who wants to extend it. In addition to that it creates the directory where to store the log file. Note that before running it you must have obtained the product certificate from your Certification Authority and put it into the "cert" directory, as explained before
  • clean: cleanup the workspace
  • dist: package the application along with its source files into a gzipped tarball

The root Makefile checks if any of the variables used in the targets of the nested Makefiles is already set and if any of them isn't it sets it with a reasonable default (lines 2-18).

It of  course takes care to export them (line 1) so that they can be fetched by the nested Makefiles.

Always take care to set every environment variable that is needed by the targets of the nested Makefiles: only this way your project is kept tidy and consistent.

Let's cleanup the workspace:

make clean

We can now have a complete go:

let's start by building the application:

make

the output is as by the following (cut) snippet:

make -C src 
make[1]: Entering directory `/home/student/devel/foo/src'
make -C greet 
make[2]: Entering directory `/home/student/devel/foo/src/greet'
gcc -Wall -g -c -fPIC greet.c
...
...
make[1]: Leaving directory `/home/student/devel/foo/man'
cd cert; make ; cd ..
make[1]: Entering directory `/home/student/devel/foo/cert'
Generating RSA private key, 4096 bit long modulus
........++
...................++
e is 65537 (0x10001)
################################################################
Certificate  Signing  Request  completed.  You must now send the
cert/656104112056.csr file to your Certification Authority.
the certificate file you get back from them must be renamed into
656104112056.crt  and  put   into  the  "cert"  directory.
################################################################
Before going it is mandatory to send the Certificate Signing Request to the Corporate's CA, get the certificate and put it into the cert directory as described by the above output message.

then let's install the application - the default install path is "~/foo"

make install

the application is now installed beneath "~/foo": we can launch it as follows:

cd ~/foo/bin
export LD_LIBRARY_PATH=../lib
./foo

the output is as follows:

Hi dear, I'm foo app

last but  not least, ... let's get back to the project folder and package everything:

cd ~/devel/foo
make dist

the outcome is the foo-0.1.tar.gz gzipped tarball, where 0.1 is the version of the application.

Creating the RPM Package

Although tarball can be a convenient packaging format to exchange things between developers, we must strive to provide our software packaged using the native package manager of the Linux distribution. On Red Hat and derivatives, it is the Redhat Package Manager, broadly known as RPM.

A Very Quick Overview Of RPM

RPM manages the building and packaging as a whole within the following directory tree:

~/rpmbuild/
├── SOURCES
├── SPECS
├── BUILD
├── RPMS
└── SRPMS

where:

  • SOURCES is the directory where archives containing the source files must be put. Archives can be tar archives, either compressed or not.
  • SPECS is the directory that contains the ".spec" files: these are the files that contain all the necessary statements that describe the RPM package.
  • BUILD is the directory used during the build process, where temporary files are stored, created, moved etc.
  • RPMS is directory that holds the built RPM packages
  • SRPMS is the directory that holds the ".src.rpm" packages: these are RPM packages that provide the archive containing the source files along with the SPEC file, so providing everything is needed to rebuild the RPM package.
Never and ever develop and/or build RPM packages as the root user.
It is not really necessary to generate the RPM development tree in the use case described in this post: I'm showing you this only for the sake of completeness and so to have the chance to describe the purpose of each directory.

Generate a stub SPEC file

First and foremost let's get back to the project main directory:

cd ~/devel/foo/RPM

RPM are built by having the rpmbuild command line utility reading the SPEC file: we can easily generate a stub SPEC file as follows:

rpmdev-newspec foo

the outcome is the "foo.spec" file: we use this stub and complete it with the missing settings that are necessary to package "foo" as RPM.

Configure the SPEC file

Edit the "RPM/foo.spec" until it looks like as follows:

%{!?_version: %define _version 0.1}
%{!?_prefixdir: %define _prefixdir /usr/local}
%{!?_docsdir: %define _docsdir /usr/share/doc/foo-%{_version}}
%{!?_includesdir: %define _includesdir /usr/include/foo}
%{!?_configdir: %define _configdir /etc/foo}
%{!?_libsdir: %define _libsdir /usr/lib64/foo}
%{!?_logsdir: %define _logsdir /var/log/foo}
%{!?_mansdir: %define _mansdir /usr/share/man/man8}
%{!?_certsdir: %define _certsdir /etc/pki/product}
%{!?_product_id: %define _product_id "656104112056" }

Name:          foo
Version:       %{_version}
Release:       1%{?dist}
Summary:       Foo package

License:       LGPL
URL:           https://grimoire.carcano.ch
Source0:       foo-%{_version}.tar.gz

BuildRequires: gcc make

%description
This is the amazing foo package that greats you every time you launch it

%package -n foo-devel
Summary:        Foo package development files

%description -n foo-devel
These are the amazing foo package develpment files

%prep
%setup -q

%build
sed -i "s+#define CONFIG_FILE .*+#define CONFIG_FILE \"%{?_configdir}/foo.conf\"+" src/facilities/config.h
sed -i "s+#define LOGGING_FILE .*+#define LOGGING_FILE \"%{?_logsdir}/foo.log\"+" src/facilities/logging.h
make %{?_smp_mflags}

%install
rm -rf $RPM_BUILD_ROOT
export ROOT="%{?buildroot}"
export PREFIX="%{?_prefixdir}"
export DOCDIR="%{?_docsdir}"
export INCLUDEDIR="%{?_includesdir}"
export CONFDIR="%{?_configdir}"
export LIBDIR="%{?_libsdir}"
export LOGDIR="%{?_logsdir}"
export MANDIR="%{?_mansdir}"
export CERTDIR="%{?_certsdir}"
export PRODUCT_ID="%{?_product_id}"
export ARCH=%{_arch}
%make_install
cd ${RPM_BUILD_ROOT}%{?_libsdir}
ln -s libgreet.so libgreet.so.%{_version}
ln -s libfacilities.so libfacilities.so.%{_version}
mkdir -m 755 ${RPM_BUILD_ROOT}/etc/ld.so.conf.d
echo "%{?_libsdir}" > ${RPM_BUILD_ROOT}/etc/ld.so.conf.d/foo-%{_arch}.conf
mv ${RPM_BUILD_ROOT}/%{?_mansdir}/foo ${RPM_BUILD_ROOT}/%{?_mansdir}/foo.8
cd ${RPM_BUILD_ROOT}/%{?_mansdir}
gzip foo.8

%files
%{_configdir}/foo.conf
%{?_certsdir}/*.pem
%{_prefixdir}/bin/foo
/etc/ld.so.conf.d/foo-%{_arch}.conf
%doc   %{_docsdir}/README
%{_libsdir}/libgreet.so*
%{_libsdir}/libfacilities.so*
%{?_mansdir}/foo.8.gz
%attr(0777 root root) %dir %{_logsdir}

%files -n foo-devel 
%{_includesdir}/facilities/*.h
%{_includesdir}/greet/*.h

%post
ldconfig

%postun 
ldconfig

%changelog
* Thu Feb 17 2022 Marco Antonio Carcano <me@mydomain.tld>
First release

The most interesting parts of this SPEC file are:

  • setting and exporting the ROOT variable with the path of the buildroot RPM macro
  • setting and exporting every environment variable used inside the Makefile so that instead of installing everything in the self-contained directory it installs everything directly on the root Linux standard filesystem (lines 42-50)
  • setting and exporting the product ID environment certificate so to know the name of the certificate file to install (line 51)
  • setting and exporting the architecture family environment variable (line 52)
  • creating the necessary symlinks to the shared objects (lines 55-56)
  • creating the file with the custom path of the shared objects (lines 57-58) and reloads ldconfig so to recreate its cache (%post and %postun sections)
  • adjusting the man page into the format that is expected on the target version of the Linux distribution (lines 59-61)

Within the same directory ("RPM"), create the Makefile with the following contents:

dist:
	@sed -i "s/%define _version .*/%define _version ${VERSION}}/" foo.spec

lastly, complete the "dist" target in the root Makefile so that it looks like as follows:

dist: clean
	$(MAKE) -C RPM $(MAKECMDGOALS)
	@ rm -f log/foo.log
	@tar czf foo-${VERSION}.tar.gz \
                --transform "s,^,foo-${VERSION}/," \
                src etc lib/Makefile bin/Makefile \
                log doc man cert RPM Makefile
	install -m 0755 -d ~/rpmbuild/SOURCES
	install -m 0755 -d ~/rpmbuild/SPECS
	install -m 0644 foo-${VERSION}.tar.gz ~/rpmbuild/SOURCES
	install -m 0644 RPM/foo.spec ~/rpmbuild/SPECS
	rpmbuild -ba RPM/foo.spec

as you can see:

  • it enters the RPM directory and launches the nested Makefile (line 2)
  • same as before, it packs the sources within a gzipped tarball (lines 4-7)
  • it creates the "~/rpmbuild/SOURCES" and "~/rpmbuild/SPECS" directories (lines 8-9 )
  • it copies the gzipped tarball to "~/rpmbuild/SOURCES" (line 10)
  • it copies the SPEC file to "~/rpmbuild/SPECS" (line 11)
  • it launches the rpm building process (line 12)

With these additions, the "dist" target is now able to create the RPM and source RPM packages.

Building the RPM

Now that we have the SPEC file, and we added the rpmbuild directives to the root Makefile, we can easily build the RPM and source RPM as follows:

cd ~/devel/foo
make dist

the output is as by the following (cut) snippet:

make -C src dist
make[1]: Entering directory `/home/student/devel/foo/src'
make -C greet dist
make[2]: Entering directory `/home/student/devel/foo/src/greet'
rm -f *.o
make[2]: Leaving directory `/home/student/devel/foo/src/greet'
make -C facilities dist
...
Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/student/rpmbuild/BUILDROOT/foo-0.1-1.el7.x86_64
Wrote: /home/student/rpmbuild/SRPMS/foo-0.1-1.el8.src.rpm
Wrote: /home/student/rpmbuild/RPMS/x86_64/foo-0.1-1.el8.x86_64.rpm
Wrote: /home/student/rpmbuild/RPMS/x86_64/foo-devel-0.1-1.el8.x86_64.rpm
Wrote: /home/student/rpmbuild/RPMS/x86_64/foo-debugsource-0.1-1.el8.x86_64.rpm
Wrote: /home/student/rpmbuild/RPMS/x86_64/foo-debuginfo-0.1-1.el8.x86_64.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.Nwq79f
+ umask 022
+ cd /home/student/rpmbuild/BUILD
+ cd foo-0.1
+ /usr/bin/rm -rf /home/student/rpmbuild/BUILDROOT/foo-0.1-1.el8.x86_64
+ exit 0

as you see from the above snippet, the following files are created beneath /home/student/rpmbuild/RPMS/x86_64 directory:

  • foo-0.1-1.el8.x86_64.rpm: RPM package with the foo application
  • foo-devel-0.1-1.el8.x86_64.rpm: the SDK RPM package with the C headers necessary to who wish to develop integrations with the foo application
  • foo-debuginfo-0.1-1.el8.x86_64.rpm: debugging information needed to provide human-readable names for binary code. This package must be installed only when debugging and intstalls beneath "/usr/lib/debug" directory *.debug files with the DWARF debugging information.
  • foo-debugsource-0.1-1.el8.x86_64.rpm: source files used for compiling the binary code. They get installed into the "/usr/src/debug" directory. Note that this package is created as by Red Hat Enterprise Linux 8: on previous releases everything related to debug was put into the debuginfo RPM sub-package.
When debugging,  install debuginfo and debugsource packages to let debuggers such as GDB or LLDB relate the execution of binary code to its source code.

In addition to them, the foo-0.1-1.el8.src.rpm with the sources is written into the "~/rpmbuild/SRPMS" directory.

GPG Signing The RPM

Before publishing an RPM package into a repository from where people can download it, we must digitally sign it: by doing so, the rpm package management utility can verify that the signature matches one of the GPG public keys that have already been imported  (that means "approved") into the RPM GPG keyring of the system .

When using installation tools such as yum, dnf, zipper and such, if the key used to sign the RPM package is missing, they can even automatically download it from the online repository and prompt us if we want to trust it - if we agree, the GPG key is installed and so RPM signed with that key are considered trusted too.

The default behavior of yum and dnf is to cancel the installation process of unsigned packages. Although you can reconfigure them to skip this check, or use the "--nogpgcheck" option to make them temporarily skip it, it is not wise to install unsigned packages, ... for the same reason why it is not wise to take candy from strangers.

Generate The GPG Key

It is straightforward that to use cryptographic software we need a lot of random: if you have not installed it yet, install, start and enable to start at boot rngd:

sudo dnf install -y rng-tools
sudo systemctl start rngd
sudo systemctl enable rngd
Rngd is a daemon that exploits both environmental noise and hardware random number generators for extracting entropy: if there's enough randomness it stores the random data into the kernel's random-number entropy pool, so that data is then made available through the /dev/random and /dev/urandom character devices.

If you are working with a Red Hat (or CentOS) version lower than 8, you must install also screen:

yum install -y screen

this is needed if  you want to use gpg commands as a user you switched to using sudo, since in such a scenario gpg is not able to securely read the password you specify to unlock the key.

Working as a user after switching to it by using sudo is a very common practice: first you login via SSH using the corporate user account, and then switch to a service account (for example the one of Continuous Integration).

Signing is made using GNU Privacy Guard (GPG), so the very first thing is generating the GNU Privacy Guard (GPG) key pair used to sign the RPM packages.

The best practice is to have a different key for every operating system family: this adds an extra step when installing software (you must import the key the first time) that can save the life of the ones installing the software if they by mistake subscribe to the channel providing software for a different operating system family.
These keys are used to sign every package built by the corporation: this means that the value of the "real name" must be something that identifies the whole corporation's development facilities. For this blog post I'm using "The Grimoire Of A Modern Linux Professional (CentOS8 x86_64)"" as my real name and "ci@grimoire.carcano.ch" as email, pretending this is the email address of the corporate's continuous integration toolchain.

If you are using Red Hat 8 or CentOS 8 (like me in this post), you can exploit both the --pinentry-mode and --full-gen-key command option, so let's create the GPG key pair by entering the following command:

gpg --expert --pinentry-mode loopback --full-gen-key

the --pinentry-mode provides a workaround to the trouble that when working as a user after switching to it using sudo gpg is not able to securely read the password specified to unlock the key.

Unfortunately the gpg version available on older Red Hat or CentOS releases does not provide this option nor --full-gen-key.

If you are working on any of them as user after switching to it using sudo, the workaround is launching a screen session - simply type:

screen

then create the GPG key pair by entering the following command:

gpg --expert --gen-key

This is the transcript of my answers to the questions:

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
   (9) ECC and ECC
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
  (14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 
Requested keysize is 2048 bits
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want for the subkey? (2048) 
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
        = key expires in n days
      w = key expires in n weeks
      m = key expires in n months
      y = key expires in n years
Key is valid for? (0) 10y
Key expires at Sat 25 Oct 2031 05:00:56 PM UTC
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: The Grimoire Of A Modern Linux Professional (CentOS8 x86_64)
Email address: ci@grimoire.carcano.ch
Comment: 
You selected this USER-ID:
    "The Grimoire Of A Modern Linux Professional (CentOS8 x86_64) <ci@grimoire.carcano.ch>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o

Let's confirm typing "o", type the desired password you want to use for unlocking the key and wait for everything to complete.

Let's now have a look at the generated keys:

gpg --list-keys

the output is as follows:

gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2031-10-25
/home/grimoire/.gnupg/pubring.kbx
---------------------------------
pub rsa2048 2021-10-27 [SC] [expires: 2031-10-25]
2A6018DE3B4282CE229A897E5BA662C0EBD6F747
uid [ultimate] The Grimoire Of A Modern Linux Professional (CentOS8 x86_64) <ci@grimoire.carcano.ch>
sub rsa2048 2021-10-27 [E] [expires: 2031-10-25]
If you want to know more about digital signatures or simply want to quickly review cryptography related stuff, don't miss the post "Cryptography quick guide - understand symmetric and asymmetric cryptography and how to use it with OpenSSL".

The public key related to the private one used to sign must be published on a repository that can be accessed by everybody who needs to install RPM packages signed by us: otherwise they have no way to check the signature.

This mean that we must export the public key as follows:

gpg --export -a ci@grimoire.carcano.ch > ~/RPM-GPG-KEY-grimoire-centos8-x86_64

and publish it on the repository.

Configuring RPM To Sign With GPG

The signing feature for the rpm command line utility is provided by the rpm-sign RPM package - let's install it as follows:

sudo dnf install -y rpm-sign

then we have to configure the RPM macros of the current user; since we can have several keys within our GPG keyring, we must specify which GPG key must be used to sign RPMs: here we use "ci@grimoire.carcano.ch", the GPG key we just created:

cat << \EOF >> ~/.rpmmacros
%_gpg_name <ci@grimoire.carcano.ch>
EOF

although this is optional, we can even specify the GPG command we use to sign along with its options  - here I can specify the "--pinentry-mode loopback" since I'm using Red Hat 8 / CentOS 8:

cat << \EOF >> ~/.rpmmacros
%__gpg_sign_cmd %{__gpg} gpg --force-v3-sigs --no-armor --pinentry-mode loopback --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} --digest-algo sha256 %{__plaintext_filename}'
EOF

Sign the RPM packages

now we have setup everything and we are eventually ready to sign the packages - the rpm command is as follows:

rpm --addsign ~/rpmbuild/RPMS/x86_64/foo-0.1-1.el8.x86_64.rpm \
~/rpmbuild/RPMS/x86_64/foo-devel-0.1-1.el8.x86_64.rpm \
~/rpmbuild/RPMS/x86_64/foo-debugsource-0.1-1.el8.x86_64.rpm \
~/rpmbuild/RPMS/x86_64/foo-debuginfo-0.1-1.el8.x86_64.rpm \
~/rpmbuild/SRPMS/foo-0.1-1.el8.src.rpm

after typing the password to unlock the secret key for the signature, the RPM packages get signed.

We can of course add a "sign" target to the root Makefile of the project:

sign:
	$(info -> Makefile: digitally signing the RPM packages...)
	rpm --resign ~/rpmbuild/RPMS/x86_64/*.rpm\
		~/rpmbuild/SRPMS/*.rpm
If you are lucky enough to have Red Hat Network Satellite Server, Katello or Spacewalk, you can push the RPM packages to any of its YUM repositories: this Makefile target is also suitable to be extended to push the signed RPM package.

Now that you configured it into the Makefile, you can easily sign the package as follows:

make sign

right after typing the password to unlock the secret key for the signature, the RPM packages get signed.

Verify the Signature of the Package

Let's now pretend to be the ones that need to install the RPM package: since the delivered RPM package is signed, we must import the public key used to sign it into an RPM database:

sudo rpm --import ~/RPM-GPG-KEY-grimoire-centos8-x86_64

we are now ready to install the RPM package using "yum", "dnf" or even the simple "rpm" command tool.

If we are not interested into installing it, but we anyway wot to check the signature, simply type:

rpm --checksig ~/rpmbuild/RPMS/x86_64/foo-*.rpm \
 ~/rpmbuild/SRPMS/foo-*.rpm

the output is as follows:

/home/student/rpmbuild/RPMS/x86_64/foo-0.1-1.el8.x86_64.rpm: digests signatures OK
/home/student/rpmbuild/RPMS/x86_64/foo-debuginfo-0.1-1.el8.x86_64.rpm: digests signatures OK
/home/student/rpmbuild/RPMS/x86_64/foo-debugsource-0.1-1.el8.x86_64.rpm: digests signatures OK
/home/student/rpmbuild/RPMS/x86_64/foo-devel-0.1-1.el8.x86_64.rpm: digests signatures OK
/home/student/rpmbuild/SRPMS/foo-0.1-1.el8.src.rpm: digests signatures OK

the RPM packet is signed and the signature is valid if the word "signatures OK" appears at the end of the  line.

Footnotes

Here it ends this tutorial on how to create and package a full featured C (or C++) project. You learned everything you must know so as to create a clean and tidy structure that helps to easily extend and maintain the project. In addition to that, you learned how to create man pages and to embed documentation with the distribution package. The install function lets it be installed on a standard linux filesystem using a privileged user, but also into a self contained directory so that unprivileged users can install it. Besides the legacy gzipped tar archive, you learned how to create and sign RPM packages and generate and provide the product certificate that can be exploited by the subscription-manager.

Writing a post like this takes hours. I'm doing it for the only pleasure of sharing knowledge and thoughts, but all of this does not come for free: it is a time consuming volunteering task. This blog is not affiliated to anybody, does not show advertisements nor sells data of visitors. The only goal of this blog is to make ideas flow. So please, if you liked this post, spend a little of your time to share it on Linkedin or Twitter using the buttons below: seeing that posts are actually read is the only way I have to understand if I'm really sharing thoughts or if I'm just wasting time and I'd better give up.

2 thoughts on “Full featured C project managed by make and packaged as RPM

  1. Felipe Guacache Hurtado says:

    Marco Antonio,
    First of all, thanks for publishing this article. I was an early adopter of linux back in the day in 1993 (kernel version 0.9x, TAMU distribution) and have seen with delight how it evolved into a major player in personal computing as well as enterprise use.
    I have never seen such a complete discussion on building a linux application/package. You did not leave anything out, as far as I can tell.
    Thanks for your selfless sharing of knowledge.
    Best regards,
    Felipe Guacache Hurtado

    • Marco Antonio Carcano says:

      Hello Felipe. I wrote it with a little bit of sadness thinking about the years I was a little bit more than a teenager. In 1993 I was just fourteen, … so I started with Linux a couple of years after you (TAMU, … sound bloody crazy even only thinking to it). I’m really pleased you liked it, and honestly I wrote it hoping that it may help people younger than us, who had not the luck to live those magic years: I really love the modern stuff such as Docker, Kubernetes and such, but I really fear that youngest people are losing sight with the pillars of Linux. This is the reason I started my blog.

Leave a Reply to Felipe Guacache Hurtado Cancel Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>