Skip to content

Commit dac969b

Browse files
adrianabedondaviddavis
authored andcommitted
Add initial support for package signing
Assisted By: GPT-5.1-Codex fixes #1300
1 parent 4e8df89 commit dac969b

File tree

15 files changed

+992
-28
lines changed

15 files changed

+992
-28
lines changed

CHANGES/1300.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added (tech preview) support for signing Debian packages when uploading to a Repository.

docs/user/guides/_SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
* [Package Uploads](upload.md)
77
* [Publish Repositories](publish.md)
88
* [Signing Service Creation](signing_service.md)
9+
* [Sign Packages](sign_packages.md)
910
* [Advanced Copy](advanced_copy.md)
1011
* [Configuring Checksums](checksums.md)

docs/user/guides/sign_packages.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Sign Debian Packages
2+
3+
Sign a Debian package using a registered APT package signing service.
4+
5+
Currently, only on-upload signing is supported.
6+
7+
## On Upload
8+
9+
!!! tip "New in 3.9.0 (Tech Preview)"
10+
11+
Sign a Debian package when uploading it to a repository.
12+
13+
### Prerequisites
14+
15+
- Have an `AptPackageSigningService` registered
16+
(see the [signing service guide](site:pulp_deb/docs/user/guides/signing_service/)).
17+
- Have the V4 fingerprint of the key you want to use. The key must be accessible by the signing
18+
service you are using (the fingerprint is forwarded via `PULP_SIGNING_KEY_FINGERPRINT`).
19+
20+
### Instructions
21+
22+
1. Configure a repository to enable signing.
23+
- Both `package_signing_service` and `package_signing_fingerprint` must be set on the
24+
repository (or provided via the REST API fields with the same names).
25+
- With those fields set, every package upload to the repository will be signed by the service.
26+
- Optionally, set `package_signing_fingerprint_release_overrides` if you need different keys per
27+
dist.
28+
2. Upload a package to this repository.
29+
30+
### Example
31+
32+
```bash
33+
# Create or update a repository with signing enabled
34+
http POST $API_ROOT/repositories/deb/apt \
35+
name="MyDebRepo" \
36+
package_signing_service=$SIGNING_SERVICE_HREF \
37+
package_signing_fingerprint=$SIGNING_FINGERPRINT
38+
39+
# Upload a package
40+
pulp deb content upload \
41+
--repository ${REPOSITORY} \
42+
--file ${DEB_FILE}
43+
```
44+
45+
### Known Limitations
46+
47+
**Traffic overhead**: The signing of a package should happen inside of a Pulp worker.
48+
[By design](site:pulpcore/docs/dev/learn/plugin-concepts/#tasks),
49+
Pulp needs to temporarily commit the file to the default backend storage in order to make the Uploaded File available to the tasking system.
50+
This implies in some extra traffic, compared to a scenario where a task could process the file directly.
51+
52+
**No sign tracking**: We do not track signing information of a package.

docs/user/guides/signing_service.md

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Signing Service Creation
22

3+
## Metadata
4+
35
To sign your APT release files on your `pulp_deb` publications, you will first need to create a signing service of type `AptReleaseSigningService`.
46

5-
## Prerequisites
7+
### Prerequisites
68

79
Creating a singing service requires the following:
810

@@ -26,7 +28,7 @@ Creating a singing service requires the following:
2628
}
2729
```
2830

29-
## Example Signing Script
31+
### Example Signing Script
3032

3133
The following example signing service script is used as part of the `pulp_deb` test suite:
3234

@@ -66,7 +68,7 @@ echo { \
6668

6769
It assumes that both public and secret key for `GPG_KEY_ID="Pulp QE"` is present in the GPG home of the Pulp user and that the secret key is not protecteded by a password.
6870

69-
## Creation Steps
71+
### Creation Steps
7072

7173
1. Add the public key to your pulp users GPG home, for example, if pulp workers are running as the `pulp` user:
7274
```bash
@@ -84,3 +86,68 @@ It assumes that both public and secret key for `GPG_KEY_ID="Pulp QE"` is present
8486
pulp signing-service show --name=PulpQE | jq -r .pulp_href
8587
```
8688
5. Start [using the signing service to sign metadata](https://staging-docs.pulpproject.org/pulp_deb/docs/user/guides/publish/#metadata-signing).
89+
90+
91+
## Packages
92+
93+
!!! tip "New in 3.9.0 (Tech Preview)"
94+
95+
Package signing is available as a tech preview beginning with pulp_deb 3.9.0. Unlike metadata
96+
signing, package signing modifies the `.deb` file directly, so it uses the
97+
`deb:AptPackageSigningService` class.
98+
99+
### Prerequisites
100+
101+
- Install `debsigs` and ensure it can access the private key you want to use.
102+
- Familiarize yourself with the general signing instructions in
103+
[pulpcore](site:pulpcore/docs/admin/guides/sign-metadata/).
104+
- Make sure the public key fingerprint you provide matches the key available to `debsigs`. During
105+
package uploads the fingerprint is passed to the script via the
106+
`PULP_SIGNING_KEY_FINGERPRINT` environment variable.
107+
108+
### Instructions
109+
110+
1. Create a signing script capable of signing a Debian package with `debsigs`.
111+
- The script receives the package path as its first argument.
112+
- The script must use `PULP_SIGNING_KEY_FINGERPRINT` to select the signing key.
113+
- The script should return JSON describing the signed file:
114+
```json
115+
{"deb_package": "/absolute/path/to/signed.deb"}
116+
```
117+
2. Register the script with `pulpcore-manager add-signing-service`.
118+
- Use `--class "deb:AptPackageSigningService"`.
119+
- The public key fingerprint passed here is only used to validate the script registration.
120+
3. Retrieve the signing service `pulp_href` for later use (for example via
121+
`pulp signing-service show --name <NAME>`).
122+
123+
### Example
124+
125+
The following script illustrates how to sign packages using `debsigs`. It copies the uploaded file
126+
into a working directory (defaulting to `PULP_TEMP_WORKING_DIR` when present), signs it in place,
127+
and emits the JSON payload expected by pulp_deb.
128+
129+
```bash title="package-signing-script.sh"
130+
#!/usr/bin/env bash
131+
set -euo pipefail
132+
133+
PACKAGE_PATH=$1
134+
FINGERPRINT="${PULP_SIGNING_KEY_FINGERPRINT:?PULP_SIGNING_KEY_FINGERPRINT is required}"
135+
WORKDIR="${PULP_TEMP_WORKING_DIR:-$(mktemp -d)}"
136+
SIGNED_PATH="${WORKDIR}/$(basename "${PACKAGE_PATH}")"
137+
138+
cp "${PACKAGE_PATH}" "${SIGNED_PATH}"
139+
debsigs --sign=origin --default-key "${FINGERPRINT}" "${SIGNED_PATH}"
140+
141+
echo {"deb_package": "${SIGNED_PATH}"}
142+
```
143+
144+
```bash
145+
pulpcore-manager add-signing-service \
146+
"SimpleDebSigningService" \
147+
${SCRIPT_ABS_FILENAME} \
148+
${KEYID} \
149+
--class "deb:AptPackageSigningService"
150+
151+
pulp signing-service show --name "SimpleDebSigningService"
152+
```
153+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Generated by Django 4.2.25 on 2025-10-23 21:43
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django_lifecycle.mixins
6+
import pulpcore.app.models.base
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('deb', '0033_aptalternatecontentsource'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='AptPackageSigningService',
18+
fields=[
19+
('signingservice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.signingservice')),
20+
],
21+
options={
22+
'abstract': False,
23+
},
24+
bases=('core.signingservice',),
25+
),
26+
migrations.AddField(
27+
model_name='aptrepository',
28+
name='package_signing_fingerprint',
29+
field=models.TextField(max_length=40, null=True),
30+
),
31+
migrations.AddField(
32+
model_name='aptrepository',
33+
name='package_signing_service',
34+
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='deb.aptpackagesigningservice'),
35+
),
36+
migrations.CreateModel(
37+
name='AptRepositoryReleasePackageSigningFingerprintOverride',
38+
fields=[
39+
('pulp_id', models.UUIDField(default=pulpcore.app.models.base.pulp_uuid, editable=False, primary_key=True, serialize=False)),
40+
('pulp_created', models.DateTimeField(auto_now_add=True)),
41+
('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)),
42+
('package_signing_fingerprint', models.TextField(max_length=40)),
43+
('release_distribution', models.TextField()),
44+
('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='package_signing_fingerprint_release_overrides', to='deb.aptrepository')),
45+
],
46+
options={
47+
'unique_together': {('repository', 'release_distribution')},
48+
},
49+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
50+
),
51+
]

pulp_deb/app/models/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
SourcePackage,
1010
)
1111

12-
from .signing_service import AptReleaseSigningService
12+
from .signing_service import AptReleaseSigningService, AptPackageSigningService
1313

1414
from .content.metadata import (
1515
Release,
@@ -28,6 +28,10 @@
2828

2929
from .remote import AptRemote
3030

31-
from .repository import AptRepository, AptRepositoryReleaseServiceOverride
31+
from .repository import (
32+
AptRepository,
33+
AptRepositoryReleaseServiceOverride,
34+
AptRepositoryReleasePackageSigningFingerprintOverride,
35+
)
3236

3337
from .acs import AptAlternateContentSource

pulp_deb/app/models/repository.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
SourceIndex,
2929
SourcePackage,
3030
SourcePackageReleaseComponent,
31+
AptPackageSigningService,
3132
)
3233

3334
import logging
@@ -66,7 +67,15 @@ class AptRepository(Repository, AutoAddObjPermsMixin):
6667
signing_service = models.ForeignKey(
6768
AptReleaseSigningService, on_delete=models.PROTECT, null=True
6869
)
70+
71+
package_signing_service = models.ForeignKey(
72+
AptPackageSigningService, on_delete=models.SET_NULL, null=True
73+
)
74+
75+
package_signing_fingerprint = models.TextField(null=True, max_length=40)
76+
6977
# Implicit signing_service_release_overrides
78+
# Implicit package_signing_fingerprint_release_overrides
7079

7180
autopublish = models.BooleanField(default=False)
7281

@@ -115,6 +124,21 @@ def release_signing_service(self, release):
115124
except AptRepositoryReleaseServiceOverride.DoesNotExist:
116125
return self.signing_service
117126

127+
def release_package_signing_fingerprint(self, release):
128+
"""
129+
Return the Package Signing Fingerprint specified in the overrides if there is one for this
130+
release, else return self.package_signing_fingerprint.
131+
"""
132+
if isinstance(release, Release):
133+
release = release.distribution
134+
try:
135+
override = self.package_signing_fingerprint_release_overrides.get(
136+
release_distribution=release
137+
)
138+
return override.package_signing_fingerprint
139+
except AptRepositoryReleasePackageSigningFingerprintOverride.DoesNotExist:
140+
return self.package_signing_fingerprint
141+
118142
def initialize_new_version(self, new_version):
119143
"""
120144
Remove old metadata from the repo before performing anything else for the new version. This
@@ -149,7 +173,9 @@ class AptRepositoryReleaseServiceOverride(BaseModel):
149173
"""
150174

151175
repository = models.ForeignKey(
152-
AptRepository, on_delete=models.CASCADE, related_name="signing_service_release_overrides"
176+
AptRepository,
177+
on_delete=models.CASCADE,
178+
related_name="signing_service_release_overrides",
153179
)
154180
signing_service = models.ForeignKey(AptReleaseSigningService, on_delete=models.PROTECT)
155181
release_distribution = models.TextField()
@@ -158,6 +184,24 @@ class Meta:
158184
unique_together = (("repository", "release_distribution"),)
159185

160186

187+
class AptRepositoryReleasePackageSigningFingerprintOverride(BaseModel):
188+
"""
189+
Override the signing fingerprint that a single Release will use in this AptRepository for
190+
signing packages.
191+
"""
192+
193+
repository = models.ForeignKey(
194+
AptRepository,
195+
on_delete=models.CASCADE,
196+
related_name="package_signing_fingerprint_release_overrides",
197+
)
198+
package_signing_fingerprint = models.TextField(max_length=40)
199+
release_distribution = models.TextField()
200+
201+
class Meta:
202+
unique_together = (("repository", "release_distribution"),)
203+
204+
161205
def find_dist_components(package_ids, content_set):
162206
"""
163207
Given a list of package_ids and a content_set, this function will find all distribution-

0 commit comments

Comments
 (0)