Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Base/usr/share/man/man1/iffysign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Name

iffysign - Sign and verify

## Synopsis

```**sh
$ iffysign [--generate] [--sign] [--verify] [--pubkey-file FILE] [--pubkey PUBLIC_KEY] [--secret-key-file FILE] [--signature FILE] [--file FILE] [--force] [--untrusted-comment COMMENT] [--comment COMMENT]
```

## Description

### Introduction

`iffysign` is a dead simple tool for signing files and verifying these signatures, including trusted file comments. It is able to both verify the _integrity_ of files (that is, they haven’t been modified during transmission, e.g. while downloading them from the internet), as well as the _authenticity_ of the files (that is, they have been signed by the person holding a certain known key).

iffysign uses the [Ed25519](https://ed25519.cr.yp.to/) system, which is a state-of-the-art elliptic asymmetric signature system. iffysign has a secret key, which is used to create signatures, and a public key, which can be shared freely and is used to verify signatures created with the secret key. In comparison to similarly-strong schemes like RSA-4096, Ed25519 keys are very short. Therefore, iffysign public keys fit in 56 characters of Base64, allowing them to be transmitted via chat applications, social media posts, QR codes, NFC tags, and other low-data and/or text-only transmission mediums. This makes the keys easier to distribute via a multitude of mediums, decreasing the likelihood of an attacker intercepting and replacing a public key across all transmission paths.

iffysign is based on minisign (see below for history). The command-line options are almost completely compatible, mostly except for option defaults. The key and signature file formats are completely compatible, with two main exceptions: First, iffysign does not support password-encrypted secret keys and can only create and read unencrypted secret keys. Second, iffysign only supports the new pre-hashed signing scheme, using the identifier `ED` (capital D).

To sign a file, iffysign hashes the file’s contents using [BLAKE2b-512](https://www.blake2.net/), a modern hashing function at least as secure as SHA-3. The hashing provides the aforementioned integrity protection. The hash is then signed using the Ed25519 secret key, creating the _file signature_. Furthermore, the concatenation of the trusted comment and this signature are signed again, creating the _global signature_. This verifies the authenticity of the trusted comment _in combination with_ the file (it is not possible to transfer a trusted comment and the global signature onto another file signed with the same key). Both signatures are stored in Base64 on disk.

### Usage

iffysign provides three primary operations invoked with the `-G`, `-V`, and `-S` options: generate keys, verify signatures, and sign files, respectively. Files to be signed or verified are specified with the `-m` option. Signatures by default are named the same as the operand file, with the extension `.iffy` appended.

When signing, the `-c` and `-t` options can be used to change the default untrusted and trusted comments. The untrusted comments of newly generated key pairs cannot be modified, but freely edited in the files afterwards.

Public keys can be provided as files with the `-p` option, or as the Base64 key string directly on the command line with the `-P` option.

iffysign returns 0 on successful operation, including a valid signature. It returns 1 if the signature is invalid or some other issue occurred. It returns 2 on invalid CLI arguments.

## Options

- `--help` Display help message and exit.
- `--version` Print version.
- `-G`, `--generate` Generate a new key pair.
- `-S`, `--sign` Sign a file.
- `-V`, `--verify` Verify that a file's signature is valid.
- `-p FILE`, `--pubkey-file FILE` Path to the public key file, default `iffysign.pub`.
- `-P PUBLIC_KEY`, `--pubkey PUBLIC_KEY` Public key as base64. This is the same as the second line of public key files.
- `-s FILE`, `--secret-key-file FILE` Secret key file, default `~/.config/iffysign/iffysign.sec`.
- `-x FILE`, `--signature FILE` Signature file, default `<file>.iffy`.
- `-m FILE`, `--file FILE` File to sign or verify.
- `-f`, `--force` Force overwrite files if they already exist. This option applies to all files iffysign can write (signatures and keys).
- `-c COMMENT`, `--untrusted-comment COMMENT` **Untrusted** (not signed) comment to add when signing. **Do not use this option** unless you know what you’re doing. iffysign will emit a warning if you only use this option to avoid accidentally attaching unverifiable (and therefore mostly worthless) comments.
- `-t COMMENT`, `--comment COMMENT` Trusted comment to add when signing.

## Files

iffysign deals in three kinds of files: signatures, public keys (and public key files), and secret keys. All three file formats are text-only and use [Base64](help://man/1/base64) to encode binary data. The [minisign website](https://jedisct1.github.io/minisign/) documents most parts of the file formats including how to create them.

For unencrypted secret keys, the `kdf_algorithm` is `\0\0` (two null bytes instead of `Sc`), and salt, opslimit and memlimit are zeroed out. The BLAKE2b-256 checksum of the secret key can be all zeroes, meaning it was not filled in and should not be verified.

All files provide untrusted comments in their first line, which are not signed and may be changed arbitrarily without invalidating signatures or keys. Untrusted comments are for end user information, such as to distinguish a large collection of public key files you have received from various sources. While useful, untrusted comments, as their name implies, are not signed or verified, and may not be relied upon for authoritative information about the keys or signatures. For instance, if the untrusted comment of a public key specifies the owner’s email address, this address’s authenticity cannot be verified and an attacker may arbitrarily change the email address without changing the public key itself. This is in contrast to protocols like OpenPGP, which provide verified proof of identity. The inability to verify any data associated with the public key, including the key ID, is by design; if you wish to sign and verify identity information with the keys, you can use trusted signature comments or identity file formats with associated signatures.

The trusted comment of a signature is verified in the global signature below it. It cannot be changed without the file failing verification.

All keys used by iffysign have randomly generated key IDs. Key IDs are not signed and may be changed arbitrarily. They are mostly a convenience feature to avoid accidentally verifying signatures with an incorrect key. Key IDs are inherited from `minisign` and `signify` mostly to provide compatibility.

The default secret key is `~/.config/iffysign/iffysign.sec`. This directory (`~/.config/iffysign`) is recommended for public and secret keys alike. The default public key is `iffysign.pub` (in the current directory).

## Examples

```sh
# Generate a new key pair in the default locations (iffysign.pub and ~/.config/iffysign/iffysign.sec)
$ minisign -G

# Sign the myfile.txt file, using your default secret key at ~/.config/iffysign/iffysign.sec
$ minisign -Sm myfile.txt
# Include a trusted comment into the signature, which will also be signed
$ minisign -Sm myfile.txt -t "This comment will be signed as well"

# Verify a file’s signature residing in myfile.txt.iffy using the base64 public key string
$ minisign -Vm myfile.txt -P <pubkey>
# Verify the file with a public key file instead
$ minisign -Vm myfile.txt -p signature.pub
```

## History

iffysign is based on [minisign(1)](https://jedisct1.github.io/minisign/) and provides a mostly compatible command-line interface as well as compatible key and signature file formats. Minisign and iffysign are based on and partially compatible with OpenBSD’s [signify(1)](https://man.openbsd.org/signify), which introduced the concept of “dead simple” signing and verification with modern cryptography. signify was written in 2014 by Ted Unangst to provide package signing for OpenBSD, and more rationale has been provided in a [transcribed talk of his](https://www.openbsd.org/papers/bsdcan-signify.html). Minisign’s major advancement over signify is the introduction of trusted (i.e. signed) comments, where signify only provides untrusted comments.

iffysign’s name was chosen like signify’s as a portmanteau of the terms “sign” and “verify”.

## See Also

- [`pkg`(1)](help://man/1/pkg)
- [`base64`(1)](help://man/1/base64)
- [`checksum`(1)](help://man/1/checksum): to only verify the integrity of files, not their authenticity
4 changes: 4 additions & 0 deletions Base/usr/share/man/man1/pkg.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ It does not currently support installing and uninstalling packages. To install t
# Query the ports database for the serenity-theming package
$ pkg -q serenity-theming
```

## See Also

- [`iffysign`(1)](help://man/1/iffysign)
1 change: 1 addition & 0 deletions Meta/Lagom/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ if (BUILD_LAGOM)

lagom_utility(icc SOURCES ../../Userland/Utilities/icc.cpp LIBS LibGfx LibMain LibURL)
lagom_utility(iconv SOURCES ../../Userland/Utilities/iconv.cpp LIBS LibTextCodec LibMain)
lagom_utility(iffysign SOURCES ../../Userland/Utilities/iffysign.cpp LIBS LibCrypto LibMain)
lagom_utility(image SOURCES ../../Userland/Utilities/image.cpp LIBS LibGfx LibMain)
lagom_utility(imgcmp SOURCES ../../Userland/Utilities/imgcmp.cpp LIBS LibGfx LibMain)
lagom_utility(isobmff SOURCES ../../Userland/Utilities/isobmff.cpp LIBS LibGfx LibMain)
Expand Down
1 change: 1 addition & 0 deletions Tests/LibCrypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set(TEST_SOURCES
TestHKDF.cpp
TestHMAC.cpp
TestMGF.cpp
TestMinisign.cpp
TestOAEP.cpp
TestPBKDF2.cpp
TestPoly1305.cpp
Expand Down
145 changes: 145 additions & 0 deletions Tests/LibCrypto/TestMinisign.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright (c) 2025, kleines Filmröllchen <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/ByteBuffer.h>
#include <AK/MemoryStream.h>
#include <AK/StringView.h>
#include <LibCore/SecretString.h>
#include <LibCrypto/Minisign.h>
#include <LibTest/Macros.h>
#include <LibTest/TestCase.h>

// All variables labeled "minisign" have content created by minisign, which verifies our compatibility.
constexpr StringView public_key_file_minisign_text = R"""(untrusted comment: minisign public key FDE44BFDD77EC45A
RWRaxH7X/Uvk/etgLk05NOsAT5aNTz1d5DjHD2R3s1/URq3vnQw6R790
)"""sv;
constexpr StringView public_key_minisign_text = "RWRaxH7X/Uvk/etgLk05NOsAT5aNTz1d5DjHD2R3s1/URq3vnQw6R790"sv;
constexpr StringView secret_key_minisign_text = R"""(untrusted comment: minisign encrypted secret key
RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWsR+1/1L5P3AKeEZBVWCT2g7hvHFeF8ALiRqPDSZdINZiB1uSVxyaetgLk05NOsAT5aNTz1d5DjHD2R3s1/URq3vnQw6R790AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
)"""sv;
constexpr StringView public_key_file_text = R"""(untrusted comment: iffysign public key
RWQ7FcRc9BMU2CTaEuu+FqFwYT5OChWG7ehQgLVIVMeerG1ANDcit9Jx
)"""sv;
constexpr StringView secret_key_text = R"""(untrusted comment: iffysign unencrypted secret key
RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOxXEXPQTFNiUeh0iJJbl84yimedpdQgkFsUitcDSY4S/yAsD1uFPvSTaEuu+FqFwYT5OChWG7ehQgLVIVMeerG1ANDcit9JxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
)"""sv;

constexpr StringView example_data = "1234abc\n"sv;
constexpr StringView empty;

constexpr StringView minisign_signature_text = R"""(untrusted comment: signature from minisign secret key
RURaxH7X/Uvk/Q81zoW4nzVrc1gVOQF5PZwD9vxF7TEI6lYC1qvXP4oyPcBiF0QtMDzJZvMj3/M+rm1S0nhxQA0pNtVL3VNFJAg=
trusted comment: timestamp:1745588000 file:1234abc hashed
bRVIYO+dSrQwLhTY6/kk/0qyIb7xrzPA7qq9RPIpOYBo9hnhL0L/IW5WLSiqaA9tSY+PjYjLJQ8GFqFfZyi4AA==
)"""sv;
constexpr StringView signature_text = R"""(untrusted comment: minisign-compatible signature
RUQ7FcRc9BMU2D9h28o+9Ba3QtfQHYdyLItVT6PWt1/PN66gHjumBCqje+eeLvQckrcKQGOQ8vkKXYtzWrkslGZdH/bHlI9txAE=
trusted comment: {"filename":"Tests/LibCrypto/1234abc"}
sEbAITvxLddnp9pAU3GhMO/02dCeG7V73J8JUN0qyj9z9H7B+6bajwu73sKPcTSOLu5cBxDeX8jNVNPziHTsCQ==
)"""sv;

using namespace Crypto::Minisign;

TEST_CASE(read_write_keys)
{
auto const minisign_key_from_file = MUST(PublicKey::from_public_key_file(public_key_file_minisign_text));
EXPECT_EQ(MUST(minisign_key_from_file.to_public_key_file()), public_key_file_minisign_text);

auto const minisign_key = MUST(PublicKey::from_base64(public_key_minisign_text.bytes()));
EXPECT_EQ(minisign_key_from_file.public_key(), minisign_key.public_key());

auto const minisign_secret_key_from_file = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_minisign_text.to_byte_string().to_byte_buffer())));
auto output = MUST(minisign_secret_key_from_file.to_secret_key_file());
auto original = Core::SecretString::take_ownership(secret_key_minisign_text.to_byte_string().to_byte_buffer());
EXPECT_EQ(output.view(), original.view());

auto const key_from_file = MUST(PublicKey::from_public_key_file(public_key_file_text));
EXPECT_EQ(MUST(key_from_file.to_public_key_file()), public_key_file_text);

auto const secret_key_from_file = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_text.to_byte_string().to_byte_buffer())));
output = MUST(secret_key_from_file.to_secret_key_file());
original = Core::SecretString::take_ownership(secret_key_text.to_byte_string().to_byte_buffer());
EXPECT_EQ(output.view(), original.view());
}

TEST_CASE(generate)
{
auto const secret_key = MUST(SecretKey::generate());
PublicKey const key = secret_key;
EXPECT_EQ(key.public_key(), secret_key.public_key());
EXPECT_EQ(key.untrusted_comment().bytes_as_string_view(), secret_key.untrusted_comment().bytes_as_string_view());

// Make sure both keys are identical across serialize/deserialize roundtrips.
auto const roundtrip_secret_key = MUST(SecretKey::from_secret_key_file(MUST(secret_key.to_secret_key_file())));
EXPECT_EQ(roundtrip_secret_key.public_key(), secret_key.public_key());
auto const roundtrip_key = MUST(PublicKey::from_public_key_file(MUST(key.to_public_key_file())));
EXPECT_EQ(roundtrip_key.public_key(), key.public_key());
EXPECT_EQ(roundtrip_key.untrusted_comment().bytes_as_string_view(), key.untrusted_comment().bytes_as_string_view());
EXPECT_EQ(roundtrip_key.id(), key.id());

// Make sure the two key pairs actually belong to each other.
FixedMemoryStream data_stream { example_data.bytes() };
auto const signature = MUST(secret_key.sign(data_stream, "i am not trustworthy"_string, "i can be trusted with power and responsibility"_string));
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Valid);
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(roundtrip_key.verify(signature, data_stream)), VerificationResult::Valid);
}

TEST_CASE(sign_verify)
{
auto const minisign_key = MUST(PublicKey::from_base64(public_key_minisign_text.bytes()));
auto const minisign_secret_key = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_minisign_text.to_byte_string().to_byte_buffer())));
auto const key = MUST(PublicKey::from_public_key_file(public_key_file_text));
auto const secret_key = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_text.to_byte_string().to_byte_buffer())));

FixedMemoryStream data_stream { example_data.bytes() };
auto signature = MUST(secret_key.sign(data_stream, "i am not trustworthy"_string, "i can be trusted with power and responsibility"_string));
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Valid);
MUST(data_stream.seek(0));
EXPECT_NE(MUST(minisign_key.verify(signature, data_stream)), VerificationResult::Valid);

// Cannot verify signatures with the wrong key.
FixedMemoryStream minisign_data_stream { example_data.bytes() };
auto const signature_minisign = MUST(minisign_secret_key.sign(minisign_data_stream, {}, "example trust"_string));
MUST(minisign_data_stream.seek(0));
EXPECT_NE(MUST(key.verify(signature_minisign, minisign_data_stream)), VerificationResult::Valid);
MUST(minisign_data_stream.seek(0));
EXPECT_EQ(MUST(minisign_key.verify(signature_minisign, minisign_data_stream)), VerificationResult::Valid);

// Signature from same key does not match against different data.
FixedMemoryStream empty_data_stream { empty.bytes() };
auto const signature_empty = MUST(secret_key.sign(minisign_data_stream, {}, "empty data"_string));
MUST(data_stream.seek(0));
EXPECT_NE(MUST(key.verify(signature_empty, data_stream)), VerificationResult::Valid);

// Signature does match if untrusted comment changed.
signature.untrusted_comment() = "EVIL ATTACKER SAYS HI"_string;
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Valid);

// Signature does *not* match if trusted comment changed.
signature.trusted_comment() = "oh no, I changed the trusted comment!"_string;
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::GlobalSignatureInvalid);

// Signature does not match if key id changed.
signature = MUST(secret_key.sign(data_stream, "i am not trustworthy"_string, "i can be trusted with power and responsibility"_string));
auto key_id_copy = signature.key_id();
key_id_copy[0] = ~key_id_copy[0];
signature.set_key_id(key_id_copy);
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Invalid);

// Check previously prepared signatures.
auto const prepared_minisign_signature = MUST(Signature::from_signature_file(minisign_signature_text));
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(minisign_key.verify(prepared_minisign_signature, data_stream)), VerificationResult::Valid);
auto const prepared_signature = MUST(Signature::from_signature_file(signature_text));
MUST(data_stream.seek(0));
EXPECT_EQ(MUST(key.verify(prepared_signature, data_stream)), VerificationResult::Valid);
}
1 change: 1 addition & 0 deletions Userland/Libraries/LibCrypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ set(SOURCES
Hash/SHA1.cpp
Hash/SHA2.cpp
NumberTheory/ModularFunctions.cpp
Minisign.cpp
PK/RSA.cpp
)

Expand Down
Loading
Loading