-
Notifications
You must be signed in to change notification settings - Fork 3.3k
LibCrypto+Userland: Add iffysign, a minisign/OpenBSD signify compatible dead simple signing and verification tool using Ed25519 cryptography #25908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kleinesfilmroellchen
wants to merge
2
commits into
SerenityOS:master
Choose a base branch
from
kleinesfilmroellchen:minisign
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 |
---|---|---|
@@ -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 |
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 |
---|---|---|
@@ -0,0 +1,173 @@ | ||
/* | ||
* Copyright (c) 2025, kleines Filmröllchen <[email protected]>. | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
||
#include <AK/Assertions.h> | ||
#include <AK/ByteString.h> | ||
#include <AK/Format.h> | ||
#include <AK/JsonObject.h> | ||
#include <AK/JsonValue.h> | ||
#include <AK/String.h> | ||
#include <LibCore/ArgsParser.h> | ||
#include <LibCore/File.h> | ||
#include <LibCore/SecretString.h> | ||
#include <LibCore/StandardPaths.h> | ||
#include <LibCrypto/Minisign.h> | ||
#include <LibMain/Main.h> | ||
|
||
enum class Operation : u8 { | ||
Unspecified, | ||
Sign, | ||
Verify, | ||
GenerateKey, | ||
}; | ||
|
||
using namespace Crypto::Minisign; | ||
|
||
ErrorOr<int> serenity_main(Main::Arguments arguments) | ||
{ | ||
Operation operation { Operation::Unspecified }; | ||
ByteString public_key_file; | ||
ByteString public_key; | ||
ByteString secret_key_file; | ||
ByteString signature_file; | ||
ByteString operand_file; | ||
String untrusted_comment; | ||
String trusted_comment; | ||
bool force_overwrite { false }; | ||
|
||
// As noted in the manpage, all options that are supported by minisign(1) as well are (almost) compatible with it. | ||
// Some of our defaults are different to reflect the missing functionality and how iffysign is used in Serenity. | ||
// minisign doesn’t use long options, so our long options are freely chosen. | ||
Core::ArgsParser args_parser; | ||
args_parser.set_general_help("Sign files and verify signatures. iffysign has a partially minisign-compatible command line interface and key/signature file formats."); | ||
|
||
// Base operations. | ||
args_parser.add_option(operation, Operation::GenerateKey, "Generate a new key pair", "generate", 'G'); | ||
args_parser.add_option(operation, Operation::Sign, "Sign a file", "sign", 'S'); | ||
args_parser.add_option(operation, Operation::Verify, "Verify that a file's signature is valid", "verify", 'V'); | ||
|
||
// Options needed by some or all operations. | ||
args_parser.add_option(public_key_file, "Path to the public key file, default `iffysign.pub`", "pubkey-file", 'p', "FILE"); | ||
args_parser.add_option(public_key, "Public key as base64", "pubkey", 'P', "PUBLIC_KEY"); | ||
args_parser.add_option(secret_key_file, "Secret key file, default `~/.config/iffysign/iffysign.sec`", "secret-key-file", 's', "FILE"); | ||
args_parser.add_option(signature_file, "Signature file, default `<file>.iffy`", "signature", 'x', "FILE"); | ||
args_parser.add_option(operand_file, "File to sign or verify", "file", 'm', "FILE"); | ||
args_parser.add_option(force_overwrite, "Force overwrite files if they already exist.", "force", 'f'); | ||
|
||
// Comment options. | ||
args_parser.add_option(untrusted_comment, "UNTRUSTED (not signed) comment to add when signing. DO NOT USE THIS OPTION unless you know what you’re doing.", "untrusted-comment", 'c', "COMMENT"); | ||
args_parser.add_option(trusted_comment, "Trusted comment to add when signing.", "comment", 't', "COMMENT"); | ||
|
||
args_parser.parse(arguments); | ||
|
||
if (operation == Operation::Unspecified) { | ||
warnln("iffysign: error: no operation specified, use one of -G, -V, -S."); | ||
return 2; | ||
} | ||
if (!public_key_file.is_empty() && !public_key.is_empty()) { | ||
warnln("iffysign: error: only one of -p, -P is allowed"); | ||
return 2; | ||
} | ||
if (public_key_file.is_empty() && public_key.is_empty()) | ||
public_key_file = "./iffysign.pub"; | ||
|
||
// Users may be used to the `-c` option, which is not really what you should use. | ||
if (!untrusted_comment.is_empty() && trusted_comment.is_empty()) | ||
warnln("iffysign: warning: Only untrusted comment provided. This comment is not signed and recipients of the signature can not validate its authenticity! Consider providing a trusted comment with the `-t` option."); | ||
if (untrusted_comment.is_empty()) | ||
untrusted_comment = "minisign-compatible signature"_string; | ||
|
||
if (signature_file.is_empty() && !operand_file.is_empty()) | ||
signature_file = ByteString::formatted("{}.minisig", operand_file); | ||
|
||
if (secret_key_file.is_empty()) | ||
secret_key_file = ByteString::formatted("{}/.config/iffysign/iffysign.sec", Core::StandardPaths::home_directory()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the |
||
|
||
auto const write_mode = force_overwrite ? Core::File::OpenMode::Write | ||
: Core::File::OpenMode::MustBeNew | Core::File::OpenMode::Write; | ||
|
||
auto const get_public_key = [&] -> ErrorOr<PublicKey> { | ||
if (!public_key_file.is_empty()) { | ||
auto public_key_file_object = TRY(Core::File::open(public_key_file, Core::File::OpenMode::Read)); | ||
auto const public_key_data = TRY(public_key_file_object->read_until_eof()); | ||
return TRY(PublicKey::from_public_key_file({ public_key_data })); | ||
} | ||
if (!public_key.is_empty()) { | ||
// We made sure earlier that in this case, a literal key must have been given. | ||
return TRY(PublicKey::from_base64(public_key.bytes())); | ||
} | ||
|
||
warnln("iffysign: error: no public key specified"); | ||
exit(1); | ||
}; | ||
|
||
switch (operation) { | ||
case Operation::Verify: { | ||
auto signature_file_object = TRY(Core::File::open(signature_file, Core::File::OpenMode::Read)); | ||
auto const signature_data = TRY(signature_file_object->read_until_eof()); | ||
auto const signature = TRY(Signature::from_signature_file(StringView { signature_data })); | ||
|
||
outln("untrusted comment is: {}", signature.untrusted_comment()); | ||
|
||
auto const public_key = TRY(get_public_key()); | ||
auto operand = TRY(Core::File::open_file_or_standard_stream(operand_file, Core::File::OpenMode::Read)); | ||
auto const signature_validity = TRY(public_key.verify(signature, *operand)); | ||
|
||
switch (signature_validity) { | ||
case VerificationResult::Invalid: | ||
outln("iffysign: error: invalid signature for file {}!", operand_file); | ||
return 1; | ||
case VerificationResult::Valid: | ||
outln("valid signature for file {}\ntrusted comment is: {}\n", operand_file, signature.trusted_comment()); | ||
return 0; | ||
case VerificationResult::GlobalSignatureInvalid: | ||
outln("iffysign: error: file signature is valid for file {} but trusted comment signature is not valid!", operand_file); | ||
return 1; | ||
} | ||
break; | ||
} | ||
|
||
case Operation::Sign: { | ||
if (trusted_comment.is_empty()) { | ||
JsonObject info_structure; | ||
// TODO: Add more information, like signing and file modification timestamps, file hash, etc. | ||
info_structure.set("filename", JsonValue { operand_file.view() }); | ||
trusted_comment = TRY(TRY(String::from_byte_string(info_structure.to_byte_string())).replace("\n"sv, " "sv, ReplaceMode::All)); | ||
} | ||
|
||
auto secret_key_file_object = TRY(Core::File::open(secret_key_file, Core::File::OpenMode::Read)); | ||
auto temporary_secret_key_data = TRY(secret_key_file_object->read_until_eof()); | ||
auto const secret_key_data = Core::SecretString::take_ownership(move(temporary_secret_key_data)); | ||
auto const secret_key = TRY(SecretKey::from_secret_key_file(secret_key_data)); | ||
|
||
auto operand = TRY(Core::File::open_file_or_standard_stream(operand_file, Core::File::OpenMode::Read)); | ||
auto const signature = TRY(secret_key.sign(*operand, untrusted_comment, trusted_comment)); | ||
auto const signature_data = TRY(signature.to_signature_file()); | ||
auto const signature_file_object = TRY(Core::File::open(signature_file, write_mode)); | ||
TRY(signature_file_object->write_until_depleted(signature_data.bytes())); | ||
break; | ||
} | ||
case Operation::GenerateKey: { | ||
auto const secret_key = TRY(SecretKey::generate()); | ||
auto const secret_key_text = TRY(secret_key.to_secret_key_file()); | ||
auto public_key = PublicKey { secret_key }; | ||
public_key.set_untrusted_comment(TRY(String::formatted("iffysign public key"))); | ||
auto const public_key_text = TRY(public_key.to_public_key_file()); | ||
|
||
auto secret_key_file_object = TRY(Core::File::open(secret_key_file, write_mode)); | ||
auto public_key_file_object = TRY(Core::File::open(public_key_file, write_mode)); | ||
TRY(secret_key_file_object->write_until_depleted(secret_key_text.view().trim("\0"sv, TrimMode::Right))); | ||
TRY(public_key_file_object->write_until_depleted(public_key_text)); | ||
outln("Generated new key pair to {} and {}.", secret_key_file, public_key_file); | ||
|
||
break; | ||
} | ||
case Operation::Unspecified: | ||
VERIFY_NOT_REACHED(); | ||
} | ||
|
||
return 0; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ArgsParser
seems kind of broken in this case, sinceoperation
always ended up beingOperation::Verify
when I tested this. I'd just add a fixme and replace the enum with a triplet of booleans to work around this for now (or fixArgsParser
, but I couldn't really figure out why it does this).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me it worked? Not sure what command line you used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the issue I was having was apparently with a dangling reference in
ArgsParser
. Theaccept_value
lambda in theEnum
specialization ofadd_option
takes a reference tonew_value
, and since that's just a local copy, the reference becomes invalid afteradd_option
returns.This fixes it, so feel free to add it (or something similar) to this PR if you want. (I reworked this to explicitly construct an
Option
to be more consistent with other overloads while at it)