Code View

plugin / plugin-2.2.0.0 / src / sign / Manifest.cpp
// SPDX-License-Identifier: MIT
// sign/Manifest.cpp
#include "sign/Manifest.hh"

#include <cctype>
#include <cstdio>
#include <cstring>
#include <sstream>

#include <elf.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <cstring>

namespace fedem {
namespace sign {

// ─────────────────────────────────────────────────────────────────────────────
// readElfSection
// ─────────────────────────────────────────────────────────────────────────────
bool Manifest::readElfSection( std::string const&    path,
                                std::string const&    sectionName,
                                std::vector<uint8_t>& out,
                                std::string&          error )
{
    int fd = ::open( path.c_str(), O_RDONLY );
    if( fd < 0 )
    {
        error = "cannot open \"" + path + "\": " + std::strerror( errno );
        return false;
    }

    struct stat st{};
    if( ::fstat( fd, &st ) < 0 )
    {
        ::close( fd );
        error = "fstat failed: " + std::string( std::strerror( errno ) );
        return false;
    }
    std::size_t const fileSize = static_cast<std::size_t>( st.st_size );
    if( fileSize == 0 )
    {
        ::close( fd );
        error = "file is empty: " + path;
        return false;
    }

    void* mapped = ::mmap( nullptr, fileSize, PROT_READ, MAP_PRIVATE, fd, 0 );
    ::close( fd );
    if( mapped == MAP_FAILED )
    {
        error = "mmap failed: " + std::string( std::strerror( errno ) );
        return false;
    }

    auto const* base = static_cast<uint8_t const*>( mapped );
    bool found = false;

    auto cleanup = [&]() { ::munmap( mapped, fileSize ); };

    if( fileSize < EI_NIDENT || std::memcmp( base, ELFMAG, SELFMAG ) != 0 )
    {
        cleanup();
        error = "not an ELF file: " + path;
        return false;
    }

    auto walkSections = [&]( auto const* ehdr ) -> bool
    {
        // Use the correct Shdr type via template
        return false; // placeholder — see specialisations below
    };
    (void)walkSections;

    if( base[EI_CLASS] == ELFCLASS64 )
    {
        if( fileSize < sizeof(Elf64_Ehdr) ) { cleanup(); error = "truncated ELF64"; return false; }
        auto const& eh = *reinterpret_cast<Elf64_Ehdr const*>( base );
        if( eh.e_shoff == 0 || eh.e_shnum == 0 ) { cleanup(); error = "no sections"; return false; }
        auto const* sh = reinterpret_cast<Elf64_Shdr const*>( base + eh.e_shoff );
        auto const* strtab = reinterpret_cast<char const*>( base + sh[eh.e_shstrndx].sh_offset );
        for( int i = 0; i < eh.e_shnum && !found; ++i )
        {
            if( std::strcmp( strtab + sh[i].sh_name, sectionName.c_str() ) == 0 )
            {
                auto const* d = base + sh[i].sh_offset;
                out.assign( d, d + sh[i].sh_size );
                found = true;
            }
        }
    }
    else if( base[EI_CLASS] == ELFCLASS32 )
    {
        if( fileSize < sizeof(Elf32_Ehdr) ) { cleanup(); error = "truncated ELF32"; return false; }
        auto const& eh = *reinterpret_cast<Elf32_Ehdr const*>( base );
        auto const* sh = reinterpret_cast<Elf32_Shdr const*>( base + eh.e_shoff );
        auto const* strtab = reinterpret_cast<char const*>( base + sh[eh.e_shstrndx].sh_offset );
        for( int i = 0; i < eh.e_shnum && !found; ++i )
        {
            if( std::strcmp( strtab + sh[i].sh_name, sectionName.c_str() ) == 0 )
            {
                auto const* d = base + sh[i].sh_offset;
                out.assign( d, d + sh[i].sh_size );
                found = true;
            }
        }
    }
    else
    {
        cleanup();
        error = "unknown ELF class in " + path;
        return false;
    }

    cleanup();
    return found;
}

// ─────────────────────────────────────────────────────────────────────────────
// JSON string escaping (RFC 8259) — matches the previous dump() output for the
// characters that can appear in manifest fields.
// ─────────────────────────────────────────────────────────────────────────────
namespace {

std::string jsonEscape( std::string const& s )
{
    std::string out;
    out.reserve( s.size() + 2 );
    for( unsigned char c : s )
    {
        switch( c )
        {
            case '"':  out += "\\\""; break;
            case '\\': out += "\\\\"; break;
            case '\b': out += "\\b";  break;
            case '\f': out += "\\f";  break;
            case '\n': out += "\\n";  break;
            case '\r': out += "\\r";  break;
            case '\t': out += "\\t";  break;
            default:
                if( c < 0x20 )
                {
                    char buf[7];
                    std::snprintf( buf, sizeof buf, "\\u%04x", c );
                    out += buf;
                }
                else
                {
                    out += static_cast<char>( c );
                }
        }
    }
    return out;
}

} // namespace

// ─────────────────────────────────────────────────────────────────────────────
// toJson — canonical serialisation (fixed key order).
//
// Output is byte-identical to the previous JSON dump(2): two-space
// indentation, "key": value, keys in the order below. The manifest is embedded
// verbatim into the DSO and signed over those exact bytes, so this format MUST
// remain stable. Strict JSON (a subset of FSON), so fromJson reads it fine.
// ─────────────────────────────────────────────────────────────────────────────
std::string Manifest::toJson( ManifestData const& d )
{
    std::ostringstream o;
    o << "{\n"
      << "  \"name\": \""        << jsonEscape( d.name )        << "\",\n"
      << "  \"version\": \""     << jsonEscape( d.version )     << "\",\n"
      << "  \"author\": \""      << jsonEscape( d.author )      << "\",\n"
      << "  \"sha256\": \""      << jsonEscape( d.sha256 )      << "\",\n"
      << "  \"publicKeyId\": \"" << jsonEscape( d.publicKeyId ) << "\",\n"
      << "  \"abiMajor\": "      << d.abiMajor                  << ",\n"
      << "  \"abiMinor\": "      << d.abiMinor                  << ",\n"
      << "  \"timestamp\": \""   << jsonEscape( d.timestamp )   << "\"\n"
      << "}";
    return o.str();
}

// ─────────────────────────────────────────────────────────────────────────────
// fromJson
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// fromJson — parse a flat manifest object.
//
// Accepts strict JSON (what toJson emits) and is tolerant of the FSON
// conveniences that could appear if a manifest were hand-edited: // and
// /* */ comments, and bare (unquoted) keys. The manifest is a flat object of
// eight scalar members, so a small hand-written scanner is enough and keeps
// the signing library free of any JSON/FSON dependency (it links only
// OpenSSL).
// ─────────────────────────────────────────────────────────────────────────────
namespace {

// Strip // line and /* block */ comments (outside string literals).
std::string stripComments( std::string const& in )
{
    std::string out;
    out.reserve( in.size() );
    bool inStr = false;
    for( std::size_t i = 0; i < in.size(); ++i )
    {
        char c = in[i];
        if( inStr )
        {
            out += c;
            if( c == '\\' && i + 1 < in.size() ) { out += in[++i]; }
            else if( c == '"' )                  { inStr = false; }
            continue;
        }
        if( c == '"' ) { inStr = true; out += c; continue; }
        if( c == '/' && i + 1 < in.size() && in[i+1] == '/' )
        {
            i += 2;
            while( i < in.size() && in[i] != '\n' ) ++i;
            if( i < in.size() ) out += '\n';
            continue;
        }
        if( c == '/' && i + 1 < in.size() && in[i+1] == '*' )
        {
            i += 2;
            while( i + 1 < in.size() && !( in[i] == '*' && in[i+1] == '/' ) ) ++i;
            i += 1;
            continue;
        }
        out += c;
    }
    return out;
}

std::string jsonUnescape( std::string const& s )
{
    std::string out;
    out.reserve( s.size() );
    for( std::size_t i = 0; i < s.size(); ++i )
    {
        if( s[i] == '\\' && i + 1 < s.size() )
        {
            char n = s[++i];
            switch( n )
            {
                case '"':  out += '"';  break;
                case '\\': out += '\\'; break;
                case '/':  out += '/';  break;
                case 'b':  out += '\b'; break;
                case 'f':  out += '\f'; break;
                case 'n':  out += '\n'; break;
                case 'r':  out += '\r'; break;
                case 't':  out += '\t'; break;
                case 'u':
                    // Manifest fields are ASCII in practice; copy the escape
                    // through unchanged rather than decoding UTF-16.
                    out += "\\u";
                    for( int k = 0; k < 4 && i + 1 < s.size(); ++k ) out += s[++i];
                    break;
                default:   out += n;    break;
            }
        }
        else
        {
            out += s[i];
        }
    }
    return out;
}

} // namespace

bool Manifest::fromJson( std::string const& json,
                          ManifestData&      out,
                          std::string&       error )
{
    std::string const s = stripComments( json );

    auto findString = [&]( char const* key, std::string& dst ) -> void
    {
        // match  "key"  or  key   followed by ':' then a "..." value
        std::string const q = std::string( "\"" ) + key + "\"";
        std::size_t k = s.find( q );
        std::size_t keyLen = q.size();
        if( k == std::string::npos )
        {
            k = s.find( key );           // bare key fallback
            keyLen = std::strlen( key );
            if( k == std::string::npos ) return;
        }
        std::size_t colon = s.find( ':', k + keyLen );
        if( colon == std::string::npos ) return;
        std::size_t open = s.find( '"', colon );
        if( open == std::string::npos ) return;
        std::size_t p = open + 1;
        std::string raw;
        for( ; p < s.size(); ++p )
        {
            if( s[p] == '\\' && p + 1 < s.size() ) { raw += s[p]; raw += s[p+1]; ++p; }
            else if( s[p] == '"' )                 { break; }
            else                                    { raw += s[p]; }
        }
        dst = jsonUnescape( raw );
    };

    auto findInt = [&]( char const* key, int& dst ) -> void
    {
        std::string const q = std::string( "\"" ) + key + "\"";
        std::size_t k = s.find( q );
        std::size_t keyLen = q.size();
        if( k == std::string::npos )
        {
            k = s.find( key );
            keyLen = std::strlen( key );
            if( k == std::string::npos ) return;
        }
        std::size_t colon = s.find( ':', k + keyLen );
        if( colon == std::string::npos ) return;
        std::size_t p = colon + 1;
        while( p < s.size() && std::isspace( static_cast<unsigned char>( s[p] ) ) ) ++p;
        bool neg = false;
        if( p < s.size() && ( s[p] == '+' || s[p] == '-' ) ) { neg = s[p] == '-'; ++p; }
        long val = 0; bool any = false;
        while( p < s.size() && std::isdigit( static_cast<unsigned char>( s[p] ) ) )
        {
            val = val * 10 + ( s[p] - '0' ); ++p; any = true;
        }
        if( any ) dst = static_cast<int>( neg ? -val : val );
    };

    // A manifest must at least contain a name; treat its absence as a parse
    // failure so corrupt sections are rejected.
    std::string probe;
    findString( "name", probe );
    if( probe.empty() )
    {
        error = "manifest parse: missing or empty 'name'";
        return false;
    }

    out.name = probe;
    findString( "version",     out.version );
    findString( "author",      out.author );
    findString( "sha256",      out.sha256 );
    findString( "publicKeyId", out.publicKeyId );
    findInt   ( "abiMajor",    out.abiMajor );
    findInt   ( "abiMinor",    out.abiMinor );
    findString( "timestamp",   out.timestamp );
    return true;
}

// ─────────────────────────────────────────────────────────────────────────────
// read
// ─────────────────────────────────────────────────────────────────────────────
ManifestResult Manifest::read( std::string const& soPath )
{
    ManifestResult result;

    std::vector<uint8_t> manifestBytes;
    std::string          manifestErr;
    if( readElfSection( soPath, kManifestSection, manifestBytes, manifestErr ) )
    {
        result.hasManifest  = true;
        result.manifestJson = std::string( manifestBytes.begin(), manifestBytes.end() );
        if( !fromJson( result.manifestJson, result.data, result.error ) )
            return result;
    }

    std::vector<uint8_t> sigBytes;
    std::string          sigErr;
    if( readElfSection( soPath, kSigSection, sigBytes, sigErr ) )
    {
        if( sigBytes.size() != ED25519_SIG_BYTES )
        {
            result.error = "invalid signature section size: "
                           + std::to_string( sigBytes.size() )
                           + " (expected 64)";
            return result;
        }
        result.hasSig = true;
        std::copy( sigBytes.begin(), sigBytes.end(), result.sig.begin() );
    }

    return result;
}

} // namespace sign
} // namespace fedem