// 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