-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: support RSA SHA256 signature verification in
phylum-init
(#165)
Actions taken: * Update the `sig` module * Remove all minisign based signature verification * Add RSA SHA256 based signature verification * Change the function names and docstrings to be more generic * This will allow for future signature method implementation changes * Raise `SystemExit` exceptions everywhere possible * Provide a useful error message * Exit with a non-zero return code * Do not clutter the output with a stack trace when the issue is known * Add a functional test * Update the unit tests Other actions taken: * Use the long form option for label * Starting with CLI v3.12.0, `--label` was added as a long form option to the `analyze` command * This new form is preferred since it is more descriptive * The minimum supported version constants were updated to reflect this * The version is just slightly newer than the one where openssl signatures were introduced (v3.10.0) and so this change is considered relevant and timely * Update Azure Pipelines docs * The example `azure-pipelines.yml` file was missing the section for PR triggers on GitHub hosted repos * There was also a typo * Format and refactor throughout Closes #141 BREAKING CHANGE: CLI installs prior to v3.12.0 are no longer supported. BREAKING CHANGE: CLI installs and upgrades can no longer be confirmed with `.minisig` minisign signatures and must instead use `.signature` RSA SHA256 based signatures.
- Loading branch information
Showing
7 changed files
with
132 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,118 +1,83 @@ | ||
"""Helper functions for verifying minisign signatures. | ||
"""Verify Phylum generated digital signatures. | ||
This module is meant to be a quick and dirty means of verifying minisign signatures in Python. | ||
There is no readily accessible Python library at this time. The `py-minisign` repository exists | ||
on GitHub to attempt this - https://github.com/x13a/py-minisign - but it does not exist as a | ||
package on PyPI and also does not appear to be actively maintained. There is a `minisign` package | ||
on PyPI - https://pypi.org/project/minisign/ - but it comes from a different repo and has no | ||
functionality at the time of this writing. | ||
This module is meant to be a simple means of verifying RSA signatures in Python. It makes use of the hazardous materials | ||
layer of the `cryptography` library, but does so in a way that closely follows the example documentation: | ||
Short of forking the `py-minisign` repo to maintain it and publish a package from it on PyPI, | ||
the actual format for signatures and public keys is simple and so is verifying signatures. | ||
https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#verification | ||
Minisign reference: https://jedisct1.github.io/minisign/ | ||
There are a number of assumptions: | ||
Even still, this module is NOT meant to be used as a library or general purpose minisign | ||
signature verification. It is purpose written to specifically verify minisign signatures that | ||
were created by Phylum. As such, it makes a number of assumptions: | ||
* The Minisign Public Key for Phylum, Inc. will not change between releases | ||
* The RSA Public Key for Phylum, Inc. will not change between releases | ||
* The RSA signature was created with the SHA256 hash function | ||
* The RSA signature was created with the PKCS1 v1.5 padding scheme | ||
* The files to be verified were created by Phylum, Inc. | ||
* The `.minisig` signature includes a trusted comment and will therefore contain a known number of lines | ||
* The source of the `.minisig` signature is a trusted location, controlled by Phylum, Inc. for it's CLI releases | ||
""" | ||
import base64 | ||
|
||
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm | ||
from cryptography.hazmat.primitives.asymmetric import ed25519 | ||
|
||
# This is the Minisign Public Key for Phylum, Inc. The matching private key was used to sign the software releases | ||
PHYLUM_MINISIGN_PUBKEY = "RWT6G44ykbS8GABiLXrJrYsap7FCY77m/Jyi0fgsr/Fsy3oLwU4l0IDf" | ||
* The source of the `.signature` files is a trusted location, controlled by Phylum, Inc. for it's CLI releases | ||
# The format for a minisign public key is: | ||
# | ||
# base64(<signature_algorithm> || <key_id> || <public_key>) | ||
# | ||
# signature_algorithm: `Ed` | ||
# key_id: 8 random bytes | ||
# public_key: Ed25519 public key | ||
PHYLUM_MINISIGN_PUBKEY_SIG_ALGO = base64.b64decode(PHYLUM_MINISIGN_PUBKEY)[:2] | ||
PHYLUM_MINISIGN_PUBKEY_KEY_ID = base64.b64decode(PHYLUM_MINISIGN_PUBKEY)[2:10] | ||
PHYLUM_MINISIGN_PUBKEY_ED25519 = base64.b64decode(PHYLUM_MINISIGN_PUBKEY)[10:] | ||
If these assumptions are not met, the signature verification will fail and the CLI install will exit with a message and | ||
a non-zero return code. The Phylum RSA public key is hard-coded in this module on purpose. It helps to limit network | ||
calls to GitHub, which can be a source of failure. It also has the advantage of "spreading" the public key to multiple | ||
locations so that a change to it (malicious or benign) will require access and coordination to each of those sources. | ||
It is understood that this method is not fool proof but should help the Phylum devs identify failures. | ||
A functional test exists to check that the hard-coded signature matches the one hosted at | ||
https://raw.githubusercontent.com/phylum-dev/cli/main/scripts/signing-key.pub since that is where the quickstart | ||
documentation directs CLI users. | ||
""" | ||
from pathlib import Path | ||
from textwrap import dedent | ||
|
||
def verify_minisig(file_path, sig_path): | ||
"""Verify a given file has a valid minisign signature. | ||
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm | ||
from cryptography.hazmat.backends.openssl import backend | ||
from cryptography.hazmat.primitives import hashes, serialization | ||
from cryptography.hazmat.primitives.asymmetric import padding, rsa, types | ||
|
||
# This is the RSA Public Key for Phylum, Inc. The matching private key was used to sign the software releases | ||
PHYLUM_RSA_PUBKEY = bytes( | ||
dedent( | ||
"""\ | ||
-----BEGIN PUBLIC KEY----- | ||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyGgvuy6CWSgJuhKY8oVz | ||
42udH1F2yIlaBoxAdQFuY2zxPSSpK9zv34B7m0JekuC5WCYfW0gS2Z8Ryu2RVdQh | ||
7DXvQb7qwzZT0H11K9Pw8hIHBvZPM+d61GWgWDc3k/rFwMmqd+kytVZy0mVxNdv4 | ||
P2qvy6BNaiUI7yoB1ahR/6klfkPit0X7pkK9sTHwW+/WcYitTQKnEnRzA3q8EmA7 | ||
rbU/sFEypzBA3C3qNJZyKSwy47kWXhC4xXUS2NXvew4FoVU6ybMoeDApwsx1AgTu | ||
CPPnPlCwuCIyUPezCP5XYczuHfaWeuwArlwdJFSUpMTc+SqO6REKgL9yvpqsO5Ia | ||
sQIDAQAB | ||
-----END PUBLIC KEY----- | ||
""" | ||
), | ||
encoding="ASCII", | ||
) | ||
|
||
|
||
def verify_sig(file_path: Path, sig_path: Path) -> None: | ||
"""Verify a given file has a valid signature. | ||
`file_path` is the path to the file data to verify. | ||
`sig_path` is the path to the `.minisig` file containing the minisign signature information. | ||
`sig_path` is the path to the `.signature` file containing the RSA SHA256 signature information. | ||
The public key is an assumed constant, the Minisign Public Key for Phylum, Inc. | ||
The public key is an assumed constant, the RSA Public Key for Phylum, Inc. | ||
""" | ||
try: | ||
phylum_public_key = ed25519.Ed25519PublicKey.from_public_bytes(PHYLUM_MINISIGN_PUBKEY_ED25519) | ||
# The `cryptography` library does not know this is an RSA public key yet...make sure it is | ||
phylum_public_key: types.PUBLIC_KEY_TYPES = serialization.load_pem_public_key(PHYLUM_RSA_PUBKEY) | ||
if isinstance(phylum_public_key, rsa.RSAPublicKey): | ||
phylum_rsa_public_key: rsa.RSAPublicKey = phylum_public_key | ||
else: | ||
raise SystemExit(f" [!] The public key was expected to be RSA but instead got: {type(phylum_public_key)}") | ||
except UnsupportedAlgorithm as err: | ||
raise RuntimeError("Ed25519 algorithm is not supported by the OpenSSL version `cryptography` is using") from err | ||
|
||
signature_algorithm, key_id, signature, trusted_comment, global_signature = extract_minisig_elements(sig_path) | ||
|
||
if signature_algorithm != b"Ed": | ||
raise RuntimeError("Only the legacy `Ed` signature algorithm is used by Phylum currently") | ||
|
||
if key_id != PHYLUM_MINISIGN_PUBKEY_KEY_ID: | ||
raise RuntimeError("The `key_id` from the `.minisig` signature did not match the `key_id` from the public key") | ||
openssl_ver = backend.openssl_version_text() | ||
msg = f" [!] Serialized key type is not supported by the OpenSSL version `cryptography` is using: {openssl_ver}" | ||
raise SystemExit(msg) from err | ||
except ValueError as err: | ||
raise SystemExit(" [!] The PEM data's structure could not be decoded successfully") from err | ||
|
||
# Confirm the trusted comment in the sig_path with the `global_signature` there | ||
# Confirm the data from `file_path` with the signature from the `sig_path` | ||
try: | ||
phylum_public_key.verify(global_signature, signature + trusted_comment) | ||
print(f" [*] Verifying {file_path} with signature from {sig_path} ...", end="") | ||
# NOTE: The verify method has no return value, but will raise an exception when the signature does not validate | ||
phylum_rsa_public_key.verify(sig_path.read_bytes(), file_path.read_bytes(), padding.PKCS1v15(), hashes.SHA256()) | ||
print("SUCCESS", flush=True) | ||
except InvalidSignature as err: | ||
raise RuntimeError("The signature could not be verified") from err | ||
|
||
# Confirm the data from file_path with the signature from the .minisig `sig_path` | ||
with open(file_path, "rb") as f: | ||
file_data = f.read() | ||
try: | ||
phylum_public_key.verify(signature, file_data) | ||
except InvalidSignature as err: | ||
raise RuntimeError("The signature could not be verified") from err | ||
|
||
|
||
def extract_minisig_elements(sig_path): | ||
"""Extract the elements from a given minisig signature file and return them.""" | ||
# The format for a minisign signature is: | ||
# | ||
# untrusted comment: <arbitrary text> | ||
# base64(<signature_algorithm> || <key_id> || <signature>) | ||
# trusted_comment: <arbitrary text> | ||
# base64(<global_signature>) | ||
# | ||
# where each line above represents a line from the `.minisig` file and the elements are defined as: | ||
# | ||
# signature_algorithm: `Ed` (legacy) or `ED` (hashed) | ||
# key_id: 8 random bytes, matching the public key | ||
# signature (legacy): ed25519(<file data>) | ||
# signature (prehashed): ed25519(Blake2b-512(<file data>)) | ||
# global_signature: ed25519(<signature> || <trusted_comment>) | ||
trusted_comment_prefix = "trusted comment: " | ||
trusted_comment_prefix_len = len(trusted_comment_prefix) | ||
ed25519_signature_len = 64 | ||
|
||
with open(sig_path, "rb") as f: | ||
lines = f.read().splitlines() | ||
if len(lines) not in (4, 5): | ||
raise RuntimeError("The .minisig file format expects 4 lines, with an optional blank 5th line") | ||
|
||
decoded_sig_line = base64.b64decode(lines[1]) | ||
signature_algorithm = decoded_sig_line[:2] | ||
key_id = decoded_sig_line[2:10] | ||
signature = decoded_sig_line[10:] | ||
if len(signature) != ed25519_signature_len: | ||
raise RuntimeError(f"The decoded signature was not {ed25519_signature_len} bytes long") | ||
|
||
trusted_comment = lines[2][trusted_comment_prefix_len:] | ||
|
||
global_signature = base64.b64decode(lines[3]) | ||
if len(global_signature) != ed25519_signature_len: | ||
raise RuntimeError(f"The global signature was not {ed25519_signature_len} bytes long") | ||
|
||
return signature_algorithm, key_id, signature, trusted_comment, global_signature | ||
print("FAIL", flush=True) | ||
raise SystemExit(" [!] The signature could not be verified and may be invalid") from err |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,40 @@ | ||
"""Test the minisign signature verification module.""" | ||
from phylum.init import sig | ||
|
||
|
||
def test_phylum_minisign_pubkey(): | ||
"""Ensure the minisign public key in use by Phylum has not changed.""" | ||
expected_key = "RWT6G44ykbS8GABiLXrJrYsap7FCY77m/Jyi0fgsr/Fsy3oLwU4l0IDf" | ||
assert sig.PHYLUM_MINISIGN_PUBKEY == expected_key, "The key should not be changing" | ||
"""Test the signature verification module.""" | ||
from textwrap import dedent | ||
|
||
from cryptography.hazmat.primitives.asymmetric import rsa | ||
from cryptography.hazmat.primitives.serialization import load_pem_public_key | ||
|
||
def test_phylum_pubkey_sig_algo(): | ||
"""Ensure the Phylum minisign public key signature algorithm is `Ed` (legacy).""" | ||
assert isinstance(sig.PHYLUM_MINISIGN_PUBKEY_SIG_ALGO, bytes) | ||
assert sig.PHYLUM_MINISIGN_PUBKEY_SIG_ALGO == b"Ed", "Only the legacy `Ed` signature is used by Phylum currently" | ||
|
||
|
||
def test_phylum_pubkey_key_id(): | ||
"""Ensure the Phylum minisign public key `key_id` has not changed.""" | ||
expected_key_id = b"\xfa\x1b\x8e2\x91\xb4\xbc\x18" | ||
assert isinstance(sig.PHYLUM_MINISIGN_PUBKEY_KEY_ID, bytes) | ||
assert sig.PHYLUM_MINISIGN_PUBKEY_KEY_ID == expected_key_id, "The key ID should not be changing" | ||
from phylum.init import sig | ||
|
||
|
||
def test_phylum_ed25519_pubkey(): | ||
"""Ensure the Phylum minisign Ed25519 public key has not changed.""" | ||
expected_key = b"\x00b-z\xc9\xad\x8b\x1a\xa7\xb1Bc\xbe\xe6\xfc\x9c\xa2\xd1\xf8,\xaf\xf1l\xcbz\x0b\xc1N%\xd0\x80\xdf" | ||
assert isinstance(sig.PHYLUM_MINISIGN_PUBKEY_ED25519, bytes) | ||
assert sig.PHYLUM_MINISIGN_PUBKEY_ED25519 == expected_key, "The Ed25519 public key should not be changing" | ||
def test_phylum_pubkey_is_bytes(): | ||
"""Ensure the RSA public key in use for the rest of these tests is a bytes object.""" | ||
assert isinstance(sig.PHYLUM_RSA_PUBKEY, bytes), "The RSA public key should be in bytes format" | ||
|
||
|
||
def test_phylum_pubkey_is_constant(): | ||
"""Ensure the RSA public key in use by Phylum has not changed.""" | ||
expected_key = bytes( | ||
dedent( | ||
"""\ | ||
-----BEGIN PUBLIC KEY----- | ||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyGgvuy6CWSgJuhKY8oVz | ||
42udH1F2yIlaBoxAdQFuY2zxPSSpK9zv34B7m0JekuC5WCYfW0gS2Z8Ryu2RVdQh | ||
7DXvQb7qwzZT0H11K9Pw8hIHBvZPM+d61GWgWDc3k/rFwMmqd+kytVZy0mVxNdv4 | ||
P2qvy6BNaiUI7yoB1ahR/6klfkPit0X7pkK9sTHwW+/WcYitTQKnEnRzA3q8EmA7 | ||
rbU/sFEypzBA3C3qNJZyKSwy47kWXhC4xXUS2NXvew4FoVU6ybMoeDApwsx1AgTu | ||
CPPnPlCwuCIyUPezCP5XYczuHfaWeuwArlwdJFSUpMTc+SqO6REKgL9yvpqsO5Ia | ||
sQIDAQAB | ||
-----END PUBLIC KEY----- | ||
""" | ||
), | ||
encoding="ASCII", | ||
) | ||
assert sig.PHYLUM_RSA_PUBKEY == expected_key, "The key should not be changing" | ||
|
||
|
||
def test_phylum_pubkey_is_rsa(): | ||
"""Ensure the public key in use by Phylum is in fact a 2048 bit RSA key.""" | ||
key = load_pem_public_key(sig.PHYLUM_RSA_PUBKEY) | ||
assert isinstance(key, rsa.RSAPublicKey) | ||
assert key.key_size == 2048 |