// SPDX-License-Identifier: MIT
// apps/src/dso-keygen.cpp
//
// dso-keygen — generate an Ed25519 developer key pair for DSO signing
//
// Usage:
// dso-keygen --name "Alice" --email "alice@example.com"
// dso-keygen --name "Alice" --email "alice@example.com"
// --out-dir keys/ --key-id alice
//
// Output:
// <key-id>.key private key (PEM, chmod 400)
// <key-id>.pub public key (PEM)
// <key-id>.fingerprint SHA-256 hex of DER public key
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iomanip>
#include <sstream>
#include <string>
namespace fs = std::filesystem;
static void fatalSsl( const char* msg )
{
std::fprintf( stderr, "error: %s\n", msg );
ERR_print_errors_fp( stderr );
std::exit( 1 );
}
static std::string hexEncode( const unsigned char* data, std::size_t len )
{
std::ostringstream ss;
ss << std::hex << std::setfill('0');
for( std::size_t i = 0; i < len; ++i )
ss << std::setw(2) << static_cast<unsigned>(data[i]);
return ss.str();
}
static std::string slugify( std::string s )
{
std::replace_if( s.begin(), s.end(),
[](char c){ return c==' '||c=='@'||c=='<'||c=='>'||c=='/'||c=='\\'; }, '-' );
std::string out; bool prev = false;
for( char c : s ) { if(c=='-'){if(!prev)out+=c;prev=true;}else{out+=c;prev=false;} }
return out;
}
int main( int argc, char** argv )
{
std::string name, email, outDir = ".", keyId;
std::string outPrivDir, outPubDir; // explicit per-role overrides
bool separate = false; // outDir/{private,public} split
for( int i = 1; i < argc; ++i )
{
if( !std::strcmp(argv[i],"--name") && i+1<argc ) name = argv[++i];
else if( !std::strcmp(argv[i],"--email") && i+1<argc ) email = argv[++i];
else if( !std::strcmp(argv[i],"--out-dir") && i+1<argc ) outDir = argv[++i];
else if( !std::strcmp(argv[i],"--out-private-dir") && i+1<argc ) outPrivDir = argv[++i];
else if( !std::strcmp(argv[i],"--out-public-dir") && i+1<argc ) outPubDir = argv[++i];
else if( !std::strcmp(argv[i],"--separate") ) separate = true;
else if( !std::strcmp(argv[i],"--key-id") && i+1<argc ) keyId = argv[++i];
else if( !std::strcmp(argv[i],"--help") || !std::strcmp(argv[i],"-h") )
{
std::puts("Usage: dso-keygen --name <name> --email <email>\n"
" [--out-dir <dir>] [--key-id <id>]\n"
" [--separate]\n"
" [--out-private-dir <dir>] [--out-public-dir <dir>]\n"
"\n"
"Output directories (private and public resolved separately):\n"
" private (.key, .fingerprint): --out-private-dir, else\n"
" <out-dir>/private when --separate, else <out-dir>\n"
" public (.pub): --out-public-dir, else\n"
" <out-dir>/public when --separate, else <out-dir>\n"
"An explicit --out-*-dir wins over --separate for that role.\n"
"Missing directories are created.");
return 0;
}
else { std::fprintf(stderr,"error: unknown: %s\n",argv[i]); return 1; }
}
if( name.empty() || email.empty() )
{ std::fputs("error: --name and --email are required\n",stderr); return 1; }
if( keyId.empty() ) keyId = slugify( name + "-" + email );
// Resolve the private and public output directories independently. An
// explicit per-role directory wins; otherwise --separate splits --out-dir
// into private/ and public/; otherwise both land in --out-dir. The
// fingerprint is a developer reference, so it goes with the private key —
// the public directory stays clean (only *.pub, which is what a runtime
// trusted-keys directory should contain).
std::string const privDir =
!outPrivDir.empty() ? outPrivDir
: ( separate ? ( outDir + "/private" ) : outDir );
std::string const pubDir =
!outPubDir.empty() ? outPubDir
: ( separate ? ( outDir + "/public" ) : outDir );
// Create any missing directories so callers need no prior mkdir.
{
std::error_code ec;
fs::create_directories( privDir, ec );
if( ec ) { std::fprintf(stderr,"error: cannot create %s: %s\n",
privDir.c_str(), ec.message().c_str()); return 1; }
fs::create_directories( pubDir, ec );
if( ec ) { std::fprintf(stderr,"error: cannot create %s: %s\n",
pubDir.c_str(), ec.message().c_str()); return 1; }
}
std::string const keyPath = privDir + "/" + keyId + ".key";
std::string const pubPath = pubDir + "/" + keyId + ".pub";
std::string const fpPath = privDir + "/" + keyId + ".fingerprint";
// Generate key pair
EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id( EVP_PKEY_ED25519, nullptr );
if( !pctx ) fatalSsl("EVP_PKEY_CTX_new_id");
if( EVP_PKEY_keygen_init(pctx) <= 0 ) fatalSsl("EVP_PKEY_keygen_init");
EVP_PKEY* pkey = nullptr;
if( EVP_PKEY_keygen(pctx,&pkey) <= 0 ) fatalSsl("EVP_PKEY_keygen");
EVP_PKEY_CTX_free( pctx );
// Write private key
{
FILE* f = std::fopen( keyPath.c_str(), "w" );
if(!f){ std::fprintf(stderr,"error: cannot write %s\n",keyPath.c_str()); return 1; }
std::fprintf(f,"# DSO developer key\n# Name: %s\n# Email: %s\n# KeyId: %s\n#\n",
name.c_str(), email.c_str(), keyId.c_str());
PEM_write_PrivateKey( f, pkey, nullptr, nullptr, 0, nullptr, nullptr );
std::fclose( f );
fs::permissions( keyPath, fs::perms::owner_read, fs::perm_options::replace );
std::printf("wrote: %s\n", keyPath.c_str());
}
// Write public key
{
FILE* f = std::fopen( pubPath.c_str(), "w" );
if(!f){ std::fprintf(stderr,"error: cannot write %s\n",pubPath.c_str()); return 1; }
std::fprintf(f,"# DSO developer public key\n# Name: %s\n# Email: %s\n# KeyId: %s\n#\n",
name.c_str(), email.c_str(), keyId.c_str());
PEM_write_PUBKEY( f, pkey );
std::fclose( f );
std::printf("wrote: %s\n", pubPath.c_str());
}
// Compute fingerprint
{
unsigned char* der = nullptr; int len = i2d_PUBKEY(pkey,&der);
if(len<=0) fatalSsl("i2d_PUBKEY");
std::size_t dlen = 0; unsigned char digest[EVP_MAX_MD_SIZE];
EVP_Q_digest(nullptr,"SHA256",nullptr,der,static_cast<std::size_t>(len),digest,&dlen);
OPENSSL_free(der);
std::string const fp = hexEncode(digest,dlen);
FILE* f = std::fopen(fpPath.c_str(),"w");
if(!f){ std::fprintf(stderr,"error: cannot write %s\n",fpPath.c_str()); return 1; }
std::fprintf(f,"%s\n",fp.c_str());
std::fclose(f);
std::printf("wrote: %s\n", fpPath.c_str());
std::printf("fingerprint: %s\n", fp.c_str());
}
EVP_PKEY_free( pkey );
std::printf("\nKey pair generated for: %s <%s>\n", name.c_str(), email.c_str());
std::printf(" private key : %s\n", keyPath.c_str());
std::printf(" public key : %s\n", pubPath.c_str());
std::printf("Trust it on a server: cp %s /etc/ffs/trusted-keys.d/\n",
pubPath.c_str());
return 0;
}