/* taler-auditor-offline will become a standalone offline signing tool for GNU Taler. It is an implementation in Go of https://git.taler.net/exchange.git/tree/src/exchange-tools/taler-auditor-offline.c in order to simplify portability (to ARM f.e.). TODOs: - implement sign operation - implement JSON-encoding of - hashes - keys - signatures - factor out types and helper functions to own package codeblau.de/taler - implement full functionality from origin */ package main import ( "bytes" "crypto/ed25519" "crypto/rsa" "encoding/binary" "encoding/json" "fmt" "log" "math/big" "os" "regexp" "strconv" "github.com/davecgh/go-spew/spew" ) type Data struct { Operation string `json:"operation"` Arguments struct { Version string `json:"version"` Currency string `json:"currency"` MasterPublicKey EdDSAPublicKey `json:"master_public_key"` ReserveClosingDelay RelativeTime `json:"reserve_closing_delay"` Signkeys []SignKey `json:"signkeys"` Denoms []DenomKey `json:"denoms"` // Recoup []??? `json:"recoup"` } `json:"arguments"` } type SignKey struct { MasterSig EdDSASignature `json:"master_sig"` StampStart AbsoluteTime `json:"stamp_start"` StampExpire AbsoluteTime `json:"stamp_expire"` StampEnd AbsoluteTime `json:"stamp_end"` Key EdDSAPublicKey `json:"key"` } type DenomKey struct { DenomPub RSAPublicKey `json:"denom_pub"` Value Amount `json:"value"` FeeWithdraw Amount `json:"fee_withdraw"` FeeDeposit Amount `json:"fee_deposit"` FeeRefresh Amount `json:"fee_refresh"` FeeRefund Amount `json:"fee_refund"` StampStart AbsoluteTime `json:"stamp_start"` StampExpireWithdraw AbsoluteTime `json:"stamp_expire_withdraw"` StampExpireDeposit AbsoluteTime `json:"stamp_expire_deposit"` StampExpireLegal AbsoluteTime `json:"stamp_expire_legal"` MasterSig EdDSASignature `json:"master_sig"` } type AbsoluteTime struct { // TODO: en-/decode "never" TMs uint64 `json:"t_ms"` } type RelativeTime struct { // TODO: en-/decode "forever" DMs uint64 `json:"d_ms"` } type EdDSAPublicKey ed25519.PublicKey func (ep *EdDSAPublicKey) UnmarshalJSON(in []byte) (e error) { var buf []byte // decode crockford.base32 if buf, e = crockfordDecode(bytes.Trim(in, `"`)); e != nil { return fmt.Errorf("couldn't decode EncodedRSAPublicKey as crockford.base32: %v (%v)", e, string(in)) } *ep = EdDSAPublicKey(buf) return nil } const ( CURRENCY_LEN = 12 CURRENCY_LEN_STR = "12" ) type Amount struct { Value uint64 `json:"value"` Fraction uint32 `json:"fraction"` Currency string `json:"currency"` } // Amount comes in as something like "CHF:0.32" // see https://docs.taler.net/core/api-common.html#amounts var nonAllowedCharsRX = regexp.MustCompile("[^a-zA-Z]") func (a *Amount) UnmarshalJSON(in []byte) (e error) { // split and parse Currency parts := bytes.Split(bytes.Trim(in, `"`), []byte(":")) if len(parts) != 2 { return fmt.Errorf("invalid amount sequence") } else if len(parts[0]) >= CURRENCY_LEN || nonAllowedCharsRX.Match(parts[0]) { return fmt.Errorf("invalid currency") } a.Currency = string(parts[0]) // split and parse Value parts = bytes.Split(parts[1], []byte(".")) if len(parts) > 2 || len(parts[0]) == 0 { return fmt.Errorf("invalid decimal value") } else if a.Value, e = strconv.ParseUint(string(parts[0]), 10, 64); e != nil { return e } else if a.Value > 2<<52 { return fmt.Errorf("decimal value too large") } // parse Fraction if len(parts) == 2 { if len(parts[1]) == 0 || len(parts[1]) > 8 { return fmt.Errorf("invalid fraction") } else if v, e := strconv.ParseUint(string(parts[1]), 10, 32); e != nil { return e } else { a.Fraction = uint32(v) } } return nil } type EdDSASignature struct { R []byte `json:"r"` S []byte `json:"s"` } // copy of GNUNET_STRINGS_string_to_data from gnunet/src/util/strings.c func crockfordDecode(enc []byte) ([]byte, error) { if len(enc) == 0 { return nil, nil } out_size := (len(enc) * 5) / 8 if out_size > 2<<24 { return nil, fmt.Errorf("input too large") } var ( out = make([]byte, out_size) encoded_len = out_size * 8 rpos = len(enc) vbit, bits, ret, shift int ) if encoded_len%5 > 0 { vbit = encoded_len % 5 // padding! !? shift = 5 - vbit rpos-- ret = getValue(enc[rpos]) bits = ret >> shift } else { vbit = 5 shift = 0 rpos-- ret = getValue(enc[rpos]) bits = ret } if ((encoded_len + shift) / 5) != len(enc) { return nil, fmt.Errorf("encoding length mismatch: %d vs %d (%v)", (encoded_len+shift)/5, len(enc), enc) } else if ret == -1 { return nil, fmt.Errorf("invalid character? getValue returned -1") } for wpos := out_size; wpos > 0; { if 0 == rpos { return nil, fmt.Errorf("incomplete encoding") } rpos-- ret = getValue(enc[rpos]) << vbit bits = ret | bits if -1 == ret { return nil, fmt.Errorf("incorrect encoding") } vbit += 5 if vbit >= 8 { wpos-- out[wpos] = byte(bits) bits >>= 8 vbit -= 8 } } if 0 != rpos || 0 != vbit { return nil, fmt.Errorf("incorrectly ended") } return out, nil } func getValue(a byte) int { // exluded letters switch a { case '0', 'o': a = '0' case 'i', 'I', 'l', 'L': a = '1' case 'u', 'U': a = 'V' } if a >= '0' && a <= '9' { return int(a - '0') } if a >= 'a' && a <= 'z' { a = a - 'a' + 'A' } dec := 0 if a >= 'A' && a <= 'Z' { if 'I' < a { dec++ } if 'L' < a { dec++ } if 'O' < a { dec++ } if 'U' < a { dec++ } return int(a) - 'A' + 10 - dec } return -1 } func (es *EdDSASignature) UnmarshalJSON(in []byte) (e error) { var buf []byte // 1. decode crockford.base32 if buf, e = crockfordDecode(bytes.Trim(in, `"`)); e != nil { return fmt.Errorf("couldn't decode EdDSASignature as crockford.base32: %v (%v)", e, string(in)) } else if len(buf) != 64 { return fmt.Errorf("unknown data as signature: %v", buf) } es.R = buf[0:32] es.S = buf[32:64] return nil } type RSAPublicKey rsa.PublicKey // following gnunet/src/json/json_helper.c and gnunet/src/util/crypto_rsa.c func (ep *RSAPublicKey) UnmarshalJSON(in []byte) (e error) { var buf []byte // 1. decode crockford.base32 if buf, e = crockfordDecode(bytes.Trim(in, `"`)); e != nil { return fmt.Errorf("couldn't decode EncodedRSAPublicKey as crockford.base32: %v (%v)", e, string(in)) } // 2. parse header if len(buf) < 4 { return fmt.Errorf("byte array too small for RSA public key header") } modulus_length, public_exponent_length := binary.BigEndian.Uint16(buf[0:]), binary.BigEndian.Uint16(buf[2:]) if len(buf[4:]) != int(modulus_length)+int(public_exponent_length) { return fmt.Errorf("byte array has wrong size according to encoded length's for modulus and public exponent") } // 3. parse RSA public key // Consult _gcry_mpi_set_buffer from libgcrypt-1.9.4/mpi/mpicoder.c buf = buf[4:] ep.N = big.NewInt(0).SetBytes(buf[:modulus_length]) buf = buf[modulus_length:] ex := big.NewInt(0).SetBytes(buf[:public_exponent_length]) // binary.BigEndian.Uint64 instead? if !ex.IsInt64() { return fmt.Errorf("public exponent is not int64") } ep.E = int(ex.Int64()) return nil } func main() { data := new(Data) dec := json.NewDecoder(os.Stdin) e := dec.Decode(data) if e != nil { log.Fatal(e) } spew.Dump(data) enc := json.NewEncoder(os.Stdout) enc.SetIndent("Data:", " ") enc.Encode(data) }