Files
opentimestamps/ots.go
2023-09-25 06:03:35 -03:00

315 lines
8.9 KiB
Go

package opentimestamps
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"strings"
"golang.org/x/exp/slices"
)
/*
* Header magic bytes
* Designed to be give the user some information in a hexdump, while being identified as 'data' by the file utility.
* \x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94
*/
var headerMagic = []byte{0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94}
var (
pendingMagic = []byte{0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e}
bitcoinMagic = []byte{0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01}
)
type Operation struct {
Name string
Tag byte
Binary bool // it's an operation that takes one argument, otherwise takes none
Apply func(curr []byte, arg []byte) []byte
}
var tags = map[byte]*Operation{
0xf0: {"append", 0xf0, true, func(curr []byte, arg []byte) []byte { return append(curr, arg...) }},
0xf1: {"prepend", 0xf1, true, func(curr []byte, arg []byte) []byte { return append(arg, curr...) }},
0xf2: {"reverse", 0xf2, false, func(curr []byte, arg []byte) []byte { panic("reverse not implemented") }},
0xf3: {"hexlify", 0xf3, false, func(curr []byte, arg []byte) []byte { panic("hexlify not implemented") }},
0x02: {"sha1", 0x02, false, func(curr []byte, arg []byte) []byte { panic("sha1 not implemented") }},
0x03: {"ripemd160", 0x03, false, func(curr []byte, arg []byte) []byte { panic("ripemd160 not implemented") }},
0x08: {"sha256", 0x08, false, func(curr []byte, arg []byte) []byte {
v := sha256.Sum256(curr)
return v[:]
}},
0x67: {"keccak256", 0x67, false, func(curr []byte, arg []byte) []byte { panic("keccak256 not implemented") }},
}
type Timestamp struct {
Digest []byte
Instructions [][]Instruction
}
type Instruction struct {
*Operation
Argument []byte
*Attestation
}
type Attestation struct {
BitcoinBlockHeight uint64
CalendarServerURL string
}
func (att Attestation) Name() string {
if att.BitcoinBlockHeight != 0 {
return "bitcoin"
} else if att.CalendarServerURL != "" {
return "pending"
} else {
return "unknown/broken"
}
}
func (att Attestation) String() string {
if att.BitcoinBlockHeight != 0 {
return fmt.Sprintf("bitcoin(%d)", att.BitcoinBlockHeight)
} else if att.CalendarServerURL != "" {
return fmt.Sprintf("pending(%s)", att.CalendarServerURL)
} else {
return "unknown/broken"
}
}
func parseCalendarServerResponse(buf Buffer, digest []byte) (*Timestamp, error) {
ts := &Timestamp{
Digest: digest,
}
err := parseTimestamp(buf, ts)
if err != nil {
return nil, err
}
return ts, nil
}
func parseOTSFile(buf Buffer) (*Timestamp, error) {
// read magic
// read version [1 byte]
// read crypto operation for file digest [1 byte]
// read file digest [32 byte (depends)]
if magic, err := buf.readBytes(len(headerMagic)); err != nil || !slices.Equal(headerMagic, magic) {
return nil, fmt.Errorf("invalid ots file header '%s': %w", magic, err)
}
if version, err := buf.readVarUint(); err != nil || version != 1 {
return nil, fmt.Errorf("invalid ots file version '%v': %w", version, err)
}
tag, err := buf.readByte()
if err != nil {
return nil, fmt.Errorf("failed to read operation byte: %w", err)
}
if op, err := readInstruction(buf, tag); err != nil || op.Operation.Name != "sha256" {
return nil, fmt.Errorf("invalid crypto operation '%v', only sha256 supported: %w", op, err)
}
// if we got here assume the digest is sha256
digest, err := buf.readBytes(32)
if err != nil {
return nil, fmt.Errorf("failed to read 32-byte digest: %w", err)
}
ts := &Timestamp{
Digest: digest,
}
if err := parseTimestamp(buf, ts); err != nil {
return nil, err
}
return ts, nil
}
func parseTimestamp(buf Buffer, ts *Timestamp) error {
// read instructions
// if operation = push
// if 0x00 = attestation
// read tag [8 bytes]
// readvarbytes
// interpret these depending on the type of attestation
// if bitcoin: readvaruint as the block height
// if pending from calendar: readvarbytes as the utf-8 calendar url
// if 0xff = skip and start reading a new block of instructions?
currInstructionsBlock := 0
ts.Instructions = make([][]Instruction, 0, 5)
// start first instruction block
ts.Instructions = append(ts.Instructions, make([]Instruction, 0, 30))
// go read these tags
for {
tag, err := buf.readByte()
if err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("failed to read operation byte: %w", err)
}
if tag == 0x00 {
// enter an attestation context
magic, err := buf.readBytes(8)
if err != nil {
return fmt.Errorf("failed to read attestion magic bytes: %w", err)
}
this, err := buf.readVarBytes()
if err != nil {
return fmt.Errorf("failed to read attestation bytes: %w", err)
}
abuf := NewBuffer(this)
switch {
case slices.Equal(magic, pendingMagic):
val, err := abuf.readVarBytes()
if err != nil {
return fmt.Errorf("failed reading calendar server url: %w", err)
}
ts.Instructions[currInstructionsBlock] = append(
ts.Instructions[currInstructionsBlock],
Instruction{Attestation: &Attestation{CalendarServerURL: string(val)}},
)
continue
case slices.Equal(magic, bitcoinMagic):
val, err := abuf.readVarUint()
if err != nil {
return fmt.Errorf("failed reading bitcoin block number: %w", err)
}
ts.Instructions[currInstructionsBlock] = append(
ts.Instructions[currInstructionsBlock],
Instruction{Attestation: &Attestation{BitcoinBlockHeight: val}},
)
continue
default:
return fmt.Errorf("unsupported attestation type %v", magic)
}
} else if tag == 0xff {
// another block of instructions
ts.Instructions = append(ts.Instructions, make([]Instruction, 0, 30))
currInstructionsBlock++
tag, err = buf.readByte()
if err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("failed to read operation byte when starting a new block of instructions: %w", err)
}
}
// a new operation in this block
inst, err := readInstruction(buf, tag)
if err != nil {
return fmt.Errorf("failed to read instruction: %w", err)
}
ts.Instructions[currInstructionsBlock] = append(ts.Instructions[currInstructionsBlock], *inst)
}
}
func readInstruction(buf Buffer, tag byte) (*Instruction, error) {
op, ok := tags[tag]
if !ok {
return nil, fmt.Errorf("unknown tag %v", tag)
}
inst := Instruction{
Operation: op,
}
if op.Binary {
val, err := buf.readVarBytes()
if err != nil {
return nil, fmt.Errorf("error reading argument: %w", err)
}
inst.Argument = val
}
return &inst, nil
}
func (ts Timestamp) String() string {
strs := make([]string, 0, 100)
strs = append(strs, fmt.Sprintf("file digest: %x", ts.Digest))
strs = append(strs, fmt.Sprintf("hashed with: sha256"))
strs = append(strs, "sets:")
for _, set := range ts.Instructions {
strs = append(strs, "~> instruction set")
for _, inst := range set {
line := " "
if inst.Operation != nil {
line += inst.Operation.Name
if inst.Operation.Binary {
line += " " + hex.EncodeToString(inst.Argument)
}
} else if inst.Attestation != nil {
line += inst.Attestation.String()
} else {
panic(fmt.Sprintf("invalid instruction timestamp: %v", inst))
}
strs = append(strs, line)
}
}
return strings.Join(strs, "\n")
}
func (ts Timestamp) SerializeToFile() []byte {
data := make([]byte, 0, 5050)
data = append(data, headerMagic...)
data = appendVarUint(data, 1)
data = append(data, 0x08) // sha256
data = append(data, ts.Digest...)
data = append(data, ts.Serialize()...)
return data
}
func (ts Timestamp) Serialize() []byte {
data := make([]byte, 0, 5000)
for i, set := range ts.Instructions {
for _, inst := range set {
if inst.Operation != nil {
// write normal operation
data = append(data, inst.Operation.Tag)
if inst.Operation.Binary {
data = appendVarBytes(data, inst.Argument)
}
} else if inst.Attestation != nil {
// write attestation record
data = append(data, 0x00)
{
// will use a new buffer for the actual attestation data
abuf := make([]byte, 0, 100)
if inst.BitcoinBlockHeight != 0 {
data = append(data, bitcoinMagic...) // this goes in the main data buffer
abuf = appendVarUint(abuf, inst.BitcoinBlockHeight)
} else if inst.CalendarServerURL != "" {
data = append(data, pendingMagic...) // this goes in the main data buffer
abuf = appendVarBytes(abuf, []byte(inst.CalendarServerURL))
} else {
panic(fmt.Sprintf("invalid attestation: %v", inst))
}
data = appendVarBytes(data, abuf) // we append that data as varbytes
}
} else {
panic(fmt.Sprintf("invalid instruction: %v", inst))
}
}
if i+1 < len(ts.Instructions) {
// write separator and start a new set of instructions
data = append(data, 0xff)
}
}
return data
}