Code View

plugin / plugin-2.2.0.0 / apps / src / dso-keygen.cpp
// 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;
}