Task 666: .SIG File Format

Task 666: .SIG File Format

File Format Specifications for .SIG

The .SIG file format refers to a detached digital signature file in the OpenPGP standard, as defined in RFC 4880 ("OpenPGP Message Format"). This format is commonly used for verifying the integrity and authenticity of associated data files (e.g., software releases). A .SIG file contains one or more OpenPGP packets, primarily a Signature Packet (tag 2), and may be in binary form or ASCII-armored (though .SIG typically denotes binary). The structure ensures cryptographic verification using public-key algorithms, hashing, and subpacket metadata for details like timestamps and key identifiers. Parsing involves reading the packet header (CTB and length), followed by the signature-specific fields and subpackets.

1. List of All Properties Intrinsic to the File Format

The following properties are inherent to the OpenPGP Signature Packet structure in a .SIG file. They represent the core elements for decoding, verification, and serialization:

  • Packet Tag: 1 byte (always 0x02 for Signature Packet).
  • Packet Length: Variable (1-5 bytes, encoded per OpenPGP length rules: short form <192, two-byte form 192-223, five-byte form with 0xFF prefix, or partial body lengths).
  • Version: 1 byte (0x03 for legacy v3 signatures or 0x04 for modern v4 signatures).
  • Signature Type: 1 byte (e.g., 0x00 for binary document signature, 0x01 for text document, 0x02 for standalone).
  • Public-Key Algorithm: 1 byte (e.g., 0x01 for RSA, 0x11 for DSA, 0x12 for ElGamal, 0x16 for ECDSA).
  • Hash Algorithm: 1 byte (e.g., 0x02 for SHA-1, 0x08 for SHA-256, 0x0A for SHA-512).
  • Creation Time (v3: 4 bytes Unix timestamp; v4: subpacket type 0x02, 4 bytes).
  • Key ID (v3: 8 bytes; v4: subpacket type 0x10, 8 bytes, or full issuer fingerprint in subpacket type 0x11, 20 bytes).
  • Expiration Time (v4 subpacket type 0x09, 4 bytes relative to creation time).
  • Revocable (v4 subpacket type 0x07, 1 byte flag).
  • Hashed Subpacket Length: 2 bytes (total length of hashed area in v4).
  • Hashed Subpackets: Variable-length array of subpackets (each: 1-2 byte scalar length + type byte [critical flag in bit 7] + data; types include 0x01 exportable, 0x03 trust signature, 0x04-0x06 regex/revocable, 0x0B preferred symmetric algos [list], 0x0C revocation key, 0x0D notation data, 0x0E preferred hash algos, 0x0F preferred compression, 0x10 issuer, 0x11 issuer fingerprint, 0x12 key server prefs, 0x13 preferred key server, 0x14 primary user ID, 0x15 policy URL, 0x16 key flags, 0x17 signer's user ID, 0x18 revocation reason, 0x19 features, 0x1A signature target, 0x1B embedded signature).
  • Unhashed Subpacket Length: 2 bytes (total length of unhashed area in v4).
  • Unhashed Subpackets: Variable-length array (same structure as hashed subpackets).
  • Hash Value (Left): 2 bytes (first two octets of the signed hash for quick verification).
  • Hash Value (Right): 2 bytes (last two octets of the signed hash).
  • Signature Value: Variable (algorithm-specific multi-precision integers [MPIs]; e.g., RSA: 1 MPI [s]; DSA: 2 MPIs [r, s]; ECDSA: 1 MPI [r || s concatenated]).

These properties ensure the signature's cryptographic validity, metadata integrity, and interoperability.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .SIG Parsing

The following is a self-contained HTML snippet embeddable in a Ghost blog post (place within a raw HTML card). It enables drag-and-drop of a .SIG file (assumes binary format), parses the OpenPGP Signature Packet, extracts and displays all listed properties on-screen, and provides a "Write" button to download a serialized copy of the parsed file.

Drag and drop a .SIG file here

This code handles basic parsing (assumes single packet, definite length, v4 preferred; extends easily for full subpacket details or MPIs). Properties are dumped as JSON for readability.

4. Python Class for .SIG Handling

The following Python class opens a .SIG file, decodes the properties, prints them to console, and writes a serialized copy.

import struct
import sys
import os

class SigParser:
    def __init__(self, filename):
        with open(filename, 'rb') as f:
            self.data = f.read()
        self.pos = 0

    def read_byte(self):
        b = self.data[self.pos]
        self.pos += 1
        return b

    def read_length(self):
        first = self.read_byte()
        if first < 192:
            return first
        elif first < 224:
            second = self.read_byte()
            return ((first - 192) * 256 + second) + 192
        else:
            # 5-byte
            self.read_byte()  # 0xFF
            return struct.unpack('<I', self.data[self.pos:self.pos+4])[0]
            self.pos += 4

    def parse(self):
        props = {}
        ctb = self.read_byte()
        props['packetTag'] = ctb & 0x3F
        if props['packetTag'] != 2:
            raise ValueError("Not a signature packet")
        props['packetLength'] = self.read_length()
        props['version'] = self.read_byte()
        props['signatureType'] = self.read_byte()
        props['pubKeyAlgo'] = self.read_byte()
        props['hashAlgo'] = self.read_byte()
        if props['version'] == 3:
            props['creationTime'] = struct.unpack('<I', self.data[self.pos:self.pos+4])[0]
            self.pos += 4
            props['keyId'] = ''.join(f'{b:02x}' for b in self.data[self.pos:self.pos+8])
            self.pos += 8
        else:
            hashed_len = struct.unpack('<H', self.data[self.pos:self.pos+2])[0]
            self.pos += 2
            props['hashedSubpacketLength'] = hashed_len
            hashed_subs = []
            sub_pos = self.pos
            while sub_pos < self.pos + hashed_len:
                sub_len_first = self.data[sub_pos]
                sub_pos += 1
                if sub_len_first > 191:
                    sub_len = ((sub_len_first - 192) * 256 + self.data[sub_pos]) + 192
                    sub_pos += 1
                else:
                    sub_len = sub_len_first
                sub_type = self.data[sub_pos] & 0x7F
                hashed_subs.append({'type': sub_type, 'length': sub_len})
                sub_pos += sub_len
            props['hashedSubpackets'] = hashed_subs
            self.pos += hashed_len
            unhashed_len = struct.unpack('<H', self.data[self.pos:self.pos+2])[0]
            self.pos += 2
            props['unhashedSubpacketLength'] = unhashed_len
            unhashed_subs = []
            sub_pos = self.pos
            while sub_pos < self.pos + unhashed_len:
                sub_len_first = self.data[sub_pos]
                sub_pos += 1
                if sub_len_first > 191:
                    sub_len = ((sub_len_first - 192) * 256 + self.data[sub_pos]) + 192
                    sub_pos += 1
                else:
                    sub_len = sub_len_first
                sub_type = self.data[sub_pos] & 0x7F
                unhashed_subs.append({'type': sub_type, 'length': sub_len})
                sub_pos += sub_len
            props['unhashedSubpackets'] = unhashed_subs
            self.pos += unhashed_len
        props['hashLeft'] = ''.join(f'{b:02x}' for b in self.data[self.pos:self.pos+2])
        self.pos += 2
        props['hashRight'] = ''.join(f'{b:02x}' for b in self.data[self.pos:self.pos+2])
        self.pos += 2
        sig_end = self.pos + (props['packetLength'] - (self.pos - 3))  # Approx
        props['signatureValue'] = ' '.join(f'{b:02x}' for b in self.data[self.pos:sig_end])
        return props

    def print_properties(self):
        props = self.parse()
        for k, v in props.items():
            print(f"{k}: {v}")

    def write(self, output_filename):
        with open(output_filename, 'wb') as f:
            f.write(self.data)
        print(f"Written to {output_filename}")

# Usage
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python sig_parser.py <file.sig>")
        sys.exit(1)
    parser = SigParser(sys.argv[1])
    parser.print_properties()
    parser.write("output.sig")

5. Java Class for .SIG Handling

The following Java class opens a .SIG file, decodes the properties, prints them to console, and writes a serialized copy. Compile and run with javac SigParser.java && java SigParser <file.sig>.

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

public class SigParser {
    private byte[] data;
    private int pos = 0;

    public SigParser(String filename) throws IOException {
        FileInputStream fis = new FileInputStream(filename);
        data = fis.readAllBytes();
        fis.close();
    }

    private int readByte() {
        return data[pos++] & 0xFF;
    }

    private int readLength() throws IOException {
        int first = readByte();
        if (first < 192) {
            return first;
        } else if (first < 224) {
            int second = readByte();
            return ((first - 192) * 256 + second) + 192;
        } else {
            readByte(); // 0xFF
            ByteBuffer bb = ByteBuffer.wrap(data, pos, 4).order(ByteOrder.LITTLE_ENDIAN);
            pos += 4;
            return bb.getInt();
        }
    }

    public Map<String, Object> parse() throws IOException {
        Map<String, Object> props = new HashMap<>();
        int ctb = readByte();
        props.put("packetTag", ctb & 0x3F);
        if ((int) props.get("packetTag") != 2) throw new IOException("Not a signature packet");
        props.put("packetLength", readLength());
        props.put("version", readByte());
        props.put("signatureType", readByte());
        props.put("pubKeyAlgo", readByte());
        props.put("hashAlgo", readByte());
        int version = (int) props.get("version");
        if (version == 3) {
            ByteBuffer bb = ByteBuffer.wrap(data, pos, 4).order(ByteOrder.LITTLE_ENDIAN);
            props.put("creationTime", bb.getInt());
            pos += 4;
            StringBuilder keyId = new StringBuilder();
            for (int i = 0; i < 8; i++) {
                keyId.append(String.format("%02x", data[pos++] & 0xFF));
            }
            props.put("keyId", keyId.toString());
        } else {
            ByteBuffer bb = ByteBuffer.wrap(data, pos, 2).order(ByteOrder.LITTLE_ENDIAN);
            int hashedLen = bb.getShort();
            pos += 2;
            props.put("hashedSubpacketLength", hashedLen);
            List<Map<String, Integer>> hashedSubs = new ArrayList<>();
            int subPos = pos;
            while (subPos < pos + hashedLen) {
                int subLenFirst = data[subPos++];
                int subLen;
                if (subLenFirst > 191) {
                    subLen = ((subLenFirst - 192) * 256 + data[subPos++]) + 192;
                } else {
                    subLen = subLenFirst;
                }
                int subType = data[subPos] & 0x7F;
                hashedSubs.add(Map.of("type", subType, "length", subLen));
                subPos += subLen;
            }
            props.put("hashedSubpackets", hashedSubs);
            pos += hashedLen;
            bb = ByteBuffer.wrap(data, pos, 2).order(ByteOrder.LITTLE_ENDIAN);
            int unhashedLen = bb.getShort();
            pos += 2;
            props.put("unhashedSubpacketLength", unhashedLen);
            List<Map<String, Integer>> unhashedSubs = new ArrayList<>();
            subPos = pos;
            while (subPos < pos + unhashedLen) {
                int subLenFirst = data[subPos++];
                int subLen;
                if (subLenFirst > 191) {
                    subLen = ((subLenFirst - 192) * 256 + data[subPos++]) + 192;
                } else {
                    subLen = subLenFirst;
                }
                int subType = data[subPos] & 0x7F;
                unhashedSubs.add(Map.of("type", subType, "length", subLen));
                subPos += subLen;
            }
            props.put("unhashedSubpackets", unhashedSubs);
            pos += unhashedLen;
        }
        StringBuilder hashLeft = new StringBuilder();
        for (int i = 0; i < 2; i++) hashLeft.append(String.format("%02x", data[pos++] & 0xFF));
        props.put("hashLeft", hashLeft.toString());
        StringBuilder hashRight = new StringBuilder();
        for (int i = 0; i < 2; i++) hashRight.append(String.format("%02x", data[pos++] & 0xFF));
        props.put("hashRight", hashRight.toString());
        int sigEnd = pos + ((int) props.get("packetLength") - (pos - 3));
        StringBuilder sigVal = new StringBuilder();
        for (int i = pos; i < sigEnd; i++) {
            sigVal.append(String.format("%02x ", data[i] & 0xFF));
        }
        props.put("signatureValue", sigVal.toString().trim());
        return props;
    }

    public void printProperties() throws IOException {
        Map<String, Object> props = parse();
        for (Map.Entry<String, Object> entry : props.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String outputFilename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(outputFilename)) {
            fos.write(data);
        }
        System.out.println("Written to " + outputFilename);
    }

    public static void main(String[] args) throws IOException {
        if (args.length < 1) {
            System.err.println("Usage: java SigParser <file.sig>");
            System.exit(1);
        }
        SigParser parser = new SigParser(args[0]);
        parser.printProperties();
        parser.write("output.sig");
    }
}

6. JavaScript Class for .SIG Handling (Node.js)

The following Node.js class opens a .SIG file, decodes the properties, prints them to console, and writes a serialized copy. Run with node sig_parser.js <file.sig> (requires fs module).

const fs = require('fs');

class SigParser {
  constructor(filename) {
    this.data = fs.readFileSync(filename);
    this.pos = 0;
  }

  readByte() {
    const b = this.data[this.pos];
    this.pos++;
    return b;
  }

  readLength() {
    let first = this.readByte();
    if (first < 192) {
      return first;
    } else if (first < 224) {
      let second = this.readByte();
      return ((first - 192) * 256 + second) + 192;
    } else {
      this.readByte(); // 0xFF
      const view = new DataView(this.data.buffer, this.pos, 4);
      const len = view.getUint32(this.pos, true);
      this.pos += 4;
      return len;
    }
  }

  parse() {
    const props = {};
    let ctb = this.readByte();
    props.packetTag = ctb & 0x3F;
    if (props.packetTag !== 2) throw new Error('Not a signature packet');
    props.packetLength = this.readLength();
    props.version = this.readByte();
    props.signatureType = this.readByte();
    props.pubKeyAlgo = this.readByte();
    props.hashAlgo = this.readByte();
    if (props.version === 3) {
      const view = new DataView(this.data.buffer, this.pos, 4);
      props.creationTime = view.getUint32(this.pos, true);
      this.pos += 4;
      let keyId = '';
      for (let i = 0; i < 8; i++) {
        keyId += this.data[this.pos++].toString(16).padStart(2, '0');
      }
      props.keyId = keyId;
    } else {
      const view = new DataView(this.data.buffer, this.pos, 2);
      let hashedLen = view.getUint16(this.pos, true);
      this.pos += 2;
      props.hashedSubpacketLength = hashedLen;
      const hashedSubs = [];
      let subPos = this.pos;
      while (subPos < this.pos + hashedLen) {
        let subLenFirst = this.data[subPos++];
        let subLen;
        if (subLenFirst > 191) {
          subLen = ((subLenFirst - 192) * 256 + this.data[subPos++]) + 192;
        } else {
          subLen = subLenFirst;
        }
        let subType = this.data[subPos] & 0x7F;
        hashedSubs.push({type: subType, length: subLen});
        subPos += subLen;
      }
      props.hashedSubpackets = hashedSubs;
      this.pos += hashedLen;
      view = new DataView(this.data.buffer, this.pos, 2);
      let unhashedLen = view.getUint16(this.pos, true);
      this.pos += 2;
      props.unhashedSubpacketLength = unhashedLen;
      const unhashedSubs = [];
      subPos = this.pos;
      while (subPos < this.pos + unhashedLen) {
        let subLenFirst = this.data[subPos++];
        let subLen;
        if (subLenFirst > 191) {
          subLen = ((subLenFirst - 192) * 256 + this.data[subPos++]) + 192;
        } else {
          subLen = subLenFirst;
        }
        let subType = this.data[subPos] & 0x7F;
        unhashedSubs.push({type: subType, length: subLen});
        subPos += subLen;
      }
      props.unhashedSubpackets = unhashedSubs;
      this.pos += unhashedLen;
    }
    let hashLeft = '';
    for (let i = 0; i < 2; i++) hashLeft += this.data[this.pos++].toString(16).padStart(2, '0');
    props.hashLeft = hashLeft;
    let hashRight = '';
    for (let i = 0; i < 2; i++) hashRight += this.data[this.pos++].toString(16).padStart(2, '0');
    props.hashRight = hashRight;
    let sigEnd = this.pos + (props.packetLength - (this.pos - 3));
    let sigVal = '';
    for (let i = this.pos; i < sigEnd; i++) {
      sigVal += this.data[i].toString(16).padStart(2, '0') + ' ';
    }
    props.signatureValue = sigVal.trim();
    return props;
  }

  printProperties() {
    const props = this.parse();
    for (const [k, v] of Object.entries(props)) {
      console.log(`${k}: ${v}`);
    }
  }

  write(outputFilename) {
    fs.writeFileSync(outputFilename, this.data);
    console.log(`Written to ${outputFilename}`);
  }
}

// Usage
const args = process.argv.slice(2);
if (args.length < 1) {
  console.error('Usage: node sig_parser.js <file.sig>');
  process.exit(1);
}
const parser = new SigParser(args[0]);
parser.printProperties();
parser.write('output.sig');

7. C Class (Struct) for .SIG Handling

The following C code defines a struct-based "class" for .SIG handling, opens the file, decodes the properties, prints them to console, and writes a serialized copy. Compile with gcc sig_parser.c -o sig_parser && ./sig_parser <file.sig>.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    unsigned char *data;
    size_t pos;
    size_t len;
} SigParser;

SigParser *sig_parser_new(const char *filename) {
    FILE *f = fopen(filename, "rb");
    if (!f) return NULL;
    fseek(f, 0, SEEK_END);
    size_t size = ftell(f);
    fseek(f, 0, SEEK_SET);
    unsigned char *buf = malloc(size);
    fread(buf, 1, size, f);
    fclose(f);
    SigParser *p = malloc(sizeof(SigParser));
    p->data = buf;
    p->pos = 0;
    p->len = size;
    return p;
}

void sig_parser_free(SigParser *p) {
    free(p->data);
    free(p);
}

uint8_t read_byte(SigParser *p) {
    return p->data[p->pos++];
}

size_t read_length(SigParser *p) {
    uint8_t first = read_byte(p);
    if (first < 192) {
        return first;
    } else if (first < 224) {
        uint8_t second = read_byte(p);
        return ((first - 192) * 256 + second) + 192;
    } else {
        read_byte(p); // 0xFF
        uint32_t len = 0;
        for (int i = 0; i < 4; i++) {
            len |= (read_byte(p) << (i * 8));
        }
        return len;
    }
}

void parse_sig(SigParser *p, char *props[20][100]) {  // Simple key-value array
    int idx = 0;
    uint8_t ctb = read_byte(p);
    sprintf(props[idx++], "packetTag: %u", ctb & 0x3F);
    if ((ctb & 0x3F) != 2) { fprintf(stderr, "Not a signature packet\n"); return; }
    size_t pkt_len = read_length(p);
    sprintf(props[idx++], "packetLength: %zu", pkt_len);
    uint8_t ver = read_byte(p);
    sprintf(props[idx++], "version: %u", ver);
    uint8_t sig_type = read_byte(p);
    sprintf(props[idx++], "signatureType: %u", sig_type);
    uint8_t pub_algo = read_byte(p);
    sprintf(props[idx++], "pubKeyAlgo: %u", pub_algo);
    uint8_t hash_algo = read_byte(p);
    sprintf(props[idx++], "hashAlgo: %u", hash_algo);
    if (ver == 3) {
        uint32_t create_time = 0;
        for (int i = 0; i < 4; i++) {
            create_time |= (read_byte(p) << (i * 8));
        }
        sprintf(props[idx++], "creationTime: %u", create_time);
        char key_id[17] = {0};
        for (int i = 0; i < 8; i++) {
            sprintf(key_id + i*2, "%02x", read_byte(p));
        }
        sprintf(props[idx++], "keyId: %s", key_id);
    } else {
        uint16_t hashed_len = (read_byte(p) | (read_byte(p) << 8));
        sprintf(props[idx++], "hashedSubpacketLength: %u", hashed_len);
        // Basic subpacket summary (hashed)
        char sub_summary[256] = "hashedSubpackets: [";
        int sub_idx = 0;
        size_t sub_pos = p->pos;
        while (sub_pos < p->pos + hashed_len) {
            uint8_t sub_len_f = p->data[sub_pos++];
            size_t sub_len = sub_len_f;
            if (sub_len_f > 191) {
                sub_len = ((sub_len_f - 192) * 256 + p->data[sub_pos++]) + 192;
            }
            uint8_t sub_type = p->data[sub_pos] & 0x7F;
            if (sub_idx > 0) strcat(sub_summary, ", ");
            char tmp[20]; sprintf(tmp, "{type:%u,len:%zu}", sub_type, sub_len);
            strcat(sub_summary, tmp);
            sub_pos += sub_len;
            sub_idx++;
        }
        strcat(sub_summary, "]");
        strcpy(props[idx++], sub_summary);
        p->pos += hashed_len;
        uint16_t unhashed_len = (read_byte(p) | (read_byte(p) << 8));
        sprintf(props[idx++], "unhashedSubpacketLength: %u", unhashed_len);
        // Similar for unhashed
        strcpy(sub_summary, "unhashedSubpackets: [");
        sub_idx = 0;
        sub_pos = p->pos;
        while (sub_pos < p->pos + unhashed_len) {
            uint8_t sub_len_f = p->data[sub_pos++];
            size_t sub_len = sub_len_f;
            if (sub_len_f > 191) {
                sub_len = ((sub_len_f - 192) * 256 + p->data[sub_pos++]) + 192;
            }
            uint8_t sub_type = p->data[sub_pos] & 0x7F;
            if (sub_idx > 0) strcat(sub_summary, ", ");
            char tmp[20]; sprintf(tmp, "{type:%u,len:%zu}", sub_type, sub_len);
            strcat(sub_summary, tmp);
            sub_pos += sub_len;
            sub_idx++;
        }
        strcat(sub_summary, "]");
        strcpy(props[idx++], sub_summary);
        p->pos += unhashed_len;
    }
    char hash_left[5] = {0};
    sprintf(hash_left, "%02x%02x", read_byte(p), read_byte(p));
    sprintf(props[idx++], "hashLeft: %s", hash_left);
    char hash_right[5] = {0};
    sprintf(hash_right, "%02x%02x", read_byte(p), read_byte(p));
    sprintf(props[idx++], "hashRight: %s", hash_right);
    // Signature value summary
    size_t sig_start = p->pos;
    size_t sig_end = sig_start + (pkt_len - (p->pos - 3));  // Approx
    char sig_hex[1024] = "signatureValue: ";
    for (size_t i = sig_start; i < sig_end && i < p->len; i++) {
        char tmp[4]; sprintf(tmp, "%02x ", p->data[i]);
        strcat(sig_hex, tmp);
    }
    strcpy(props[idx++], sig_hex);
}

void print_properties(SigParser *p) {
    char props[20][100];
    parse_sig(p, props);
    for (int i = 0; i < 20 && strlen(props[i]); i++) {
        printf("%s\n", props[i]);
    }
}

void write_file(SigParser *p, const char *output) {
    FILE *f = fopen(output, "wb");
    fwrite(p->data, 1, p->len, f);
    fclose(f);
    printf("Written to %s\n", output);
}

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <file.sig>\n", argv[0]);
        return 1;
    }
    SigParser *p = sig_parser_new(argv[1]);
    if (!p) return 1;
    print_properties(p);
    write_file(p, "output.sig");
    sig_parser_free(p);
    return 0;
}

This C implementation uses manual byte manipulation for portability, summarizing subpackets and signature as hex strings for brevity. Full MPI parsing can be added if required.