Skip to content

Commit 4b1cb65

Browse files
danimtbczoido
andauthored
Add package signing plugin example using OpenSSL (#207)
* Add package signing plugin example * Update examples/extensions/plugins/sign/readme.md Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * update * minor update * update plugin example * update * fix mkdirs * fix folders * try * fix path * fix output asserts * fix leftovers * strip * fix * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * Apply suggestion from @danimtb * conan version check in test * Apply suggestion from @danimtb * Apply suggestion from @danimtb * fix test * Update examples/extensions/plugins/openssl_sign/readme.md Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * Update examples/extensions/plugins/openssl_sign/sign.py Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * Update examples/extensions/plugins/openssl_sign/sign.py Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * Update examples/extensions/plugins/openssl_sign/sign.py Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * Update examples/extensions/plugins/openssl_sign/sign.py Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * review * use warning * fixes * Rename readme.md to README.md * add note to code and readme * Update examples/extensions/plugins/openssl_sign/README.md Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> * Apply suggestion from @czoido Co-authored-by: Carlos Zoido <mrgalleta@gmail.com> --------- Co-authored-by: Carlos Zoido <mrgalleta@gmail.com>
1 parent 28e6493 commit 4b1cb65

File tree

4 files changed

+168
-2
lines changed

4 files changed

+168
-2
lines changed

examples/extensions/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# Conan extensions examples
22

3-
### [Use custom commands in your Conan CLI](extensions/commands/)
3+
### [Use custom commands in your Conan CLI](commands)
44

55
- Learn how to create custom commands in Conan. [Docs](https://docs.conan.io/2/reference/commands/custom_commands.html)
66

7-
### [Use custom deployers](extensions/deployers/)
7+
### [Use custom deployers](deployers)
88

99
- Learn how to create a custom deployer in Conan. [Docs](https://docs.conan.io/2/reference/extensions/deployers.html)
10+
11+
### [Package signing plugin example with OpenSSL](plugins/openssl_sign)
12+
13+
- Learn how to create a package signing plugin in Conan. [Docs](https://docs.conan.io/2/reference/extensions/package_signing.html)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
## Package signing plugin example with OpenSSL
3+
4+
> **_SECURITY NOTE:_** This example stores a private key next to the plugin for simplicity. **Do not do this in production**.
5+
> Instead, load the signing key from environment variables or a secret manager, or delegate signing to a remote signing service.
6+
> **Always keep the private key out of the Conan cache and out of source control**.
7+
8+
9+
Steps to test the example:
10+
11+
- Copy the ``sign.py`` file to your Conan home at ```CONAN_HOME/extensions/plugins/sign/sign.py```.
12+
- Generate your signing keys (see comment at the top of the ``sign.py`` file) and place them inside a folder with the name of your provider (``my-organization`` in the example) next to the ``sign.py`` file (``CONAN_HOME/extensions/plugins/sign/my-organization/<keys>``).
13+
- Generate a new project to test the sign and verify commands: ``conan new cmake_lib -d name=hello -d version=1.0``
14+
- Create the package: ``conan create``
15+
- Sign the package: ``conan cache sign hello/1.0``
16+
- Verify the package signature: ```conan cache verify hello/1.0```
17+
- You can also use the ``conan install`` command, and the packages should be verified automatically when they are downloaded from a remote.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
import shutil
3+
4+
from conan import conan_version
5+
from test.examples_tools import run
6+
7+
if conan_version >= "2.26.0-dev":
8+
current_dir = os.path.abspath(os.path.dirname(__file__))
9+
provider_folder = os.path.join(current_dir, "my-organization")
10+
11+
os.makedirs(provider_folder)
12+
run(f"openssl genpkey -algorithm RSA -out {provider_folder}/private_key.pem -pkeyopt rsa_keygen_bits:2048")
13+
run(f"openssl pkey -in {provider_folder}/private_key.pem -pubout -out {provider_folder}/public_key.pem")
14+
15+
run(f"conan config install {current_dir} -t dir --target-folder extensions/plugins/sign")
16+
17+
run("conan new cmake_lib -d name=hello -d version=1.0")
18+
run("conan create")
19+
20+
output = run("conan cache sign hello/1.0")
21+
assert "Package signed for reference hello/1.0" in output
22+
assert "[Package sign] Summary: OK=2, FAILED=0" in output
23+
output = run("conan cache verify hello/1.0")
24+
assert "Package verified for reference hello/1.0" in output
25+
assert "[Package sign] Summary: OK=2, FAILED=0" in output
26+
27+
conan_home = run("conan config home").strip()
28+
shutil.rmtree(os.path.join(conan_home, "extensions", "plugins", "sign"))
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Plugin to sign/verify Conan packages with OpenSSL.
3+
4+
You will need to have ``openssl`` installed at the system level and available in your ``PATH``.
5+
6+
To use this plugin, first generate a compatible keypair:
7+
8+
$ openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
9+
10+
And extract the public key:
11+
12+
$ openssl pkey -in private_key.pem -pubout -out public_key.pem
13+
14+
The private_key.pem and public_key.pem files should be placed inside a folder named with the the provider's name
15+
('my-organization' for this example). The 'my-organization' folder should be next to this plugins' file sign.py
16+
(inside the CONAN_HOME/extensions/plugins/sign folder).
17+
18+
SECURITY NOTE:
19+
This example stores a private key next to the plugin for simplicity. **Do not do this in production**.
20+
Instead, load the signing key from environment variables or a secret manager, or delegate signing to a remote signing service.
21+
**Always keep the private key out of the Conan cache and out of source control**.
22+
"""
23+
24+
import os
25+
import json
26+
import subprocess
27+
28+
from conan.api.output import ConanOutput
29+
from conan.errors import ConanException
30+
31+
32+
def _run_command(command):
33+
ConanOutput().info(f"Running command: {' '.join(command)}")
34+
result = subprocess.run(
35+
command,
36+
stdout=subprocess.PIPE,
37+
stderr=subprocess.PIPE,
38+
text=True, # returns strings instead of bytes
39+
check=False # we'll manually handle error checking
40+
)
41+
42+
if result.returncode != 0:
43+
raise subprocess.CalledProcessError(
44+
result.returncode, result.args, output=result.stdout, stderr=result.stderr
45+
)
46+
47+
48+
def sign(ref, artifacts_folder, signature_folder, **kwargs):
49+
provider = "my-organization" # This maps to the folder containing the signing keys (for simplicity)
50+
manifest_filepath = os.path.join(signature_folder, "pkgsign-manifest.json")
51+
signature_filename = "pkgsign-manifest.json.sig"
52+
signature_filepath = os.path.join(signature_folder, signature_filename)
53+
if os.path.isfile(signature_filepath):
54+
ConanOutput().warning(f"Package {ref.repr_notime()} was already signed")
55+
56+
privkey_filepath = os.path.join(os.path.dirname(__file__), provider, "private_key.pem")
57+
# openssl dgst -sha256 -sign private_key.pem -out document.sig document.txt
58+
openssl_sign_cmd = [
59+
"openssl",
60+
"dgst",
61+
"-sha256",
62+
"-sign", privkey_filepath,
63+
"-out", signature_filepath,
64+
manifest_filepath
65+
]
66+
try:
67+
_run_command(openssl_sign_cmd)
68+
ConanOutput().success(f"Package signed for reference {ref}")
69+
except Exception as exc:
70+
raise ConanException(f"Error signing artifact: {exc}")
71+
return [{"method": "openssl-dgst",
72+
"provider": provider,
73+
"sign_artifacts": {
74+
"manifest": "pkgsign-manifest.json",
75+
"signature": signature_filename}}]
76+
77+
78+
def verify(ref, artifacts_folder, signature_folder, files, **kwargs):
79+
signatures_path = os.path.join(signature_folder, "pkgsign-signatures.json")
80+
try:
81+
with open(signatures_path, "r", encoding="utf-8") as f:
82+
signatures = json.loads(f.read()).get("signatures")
83+
except Exception:
84+
ConanOutput().warning("Could not verify unsigned package")
85+
return
86+
87+
for signature in signatures:
88+
signature_filename = signature.get("sign_artifacts").get("signature")
89+
signature_filepath = os.path.join(signature_folder, signature_filename)
90+
if not os.path.isfile(signature_filepath):
91+
raise ConanException(f"Signature file does not exist at {signature_filepath}")
92+
93+
# The provider is useful to choose the correct public key to verify packages with
94+
provider = signature.get("provider")
95+
pubkey_filepath = os.path.join(os.path.dirname(__file__), provider, "public_key.pem")
96+
if not os.path.isfile(pubkey_filepath):
97+
raise ConanException(f"Public key not found for provider '{provider}'")
98+
99+
manifest_filepath = os.path.join(signature_folder, "pkgsign-manifest.json")
100+
signature_method = signature.get("method")
101+
if signature_method == "openssl-dgst":
102+
# openssl dgst -sha256 -verify public_key.pem -signature document.sig document.txt
103+
openssl_verify_cmd = [
104+
"openssl",
105+
"dgst",
106+
"-sha256",
107+
"-verify", pubkey_filepath,
108+
"-signature", signature_filepath,
109+
manifest_filepath,
110+
]
111+
try:
112+
_run_command(openssl_verify_cmd)
113+
ConanOutput().success(f"Package verified for reference {ref}")
114+
except Exception as exc:
115+
raise ConanException(f"Error verifying signature {signature_filepath}: {exc}")
116+
else:
117+
raise ConanException(f"Sign method {signature_method} not supported. Cannot verify package")

0 commit comments

Comments
 (0)