/* 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: - [x] implement TALER_exchange_offline_denom_validity_verify - [x] implement TALER_auditor_denom_validity_sign - [x] implement JSON-encoding of - [x] hashes - [x] keys - [x] signatures - [ ] implement full functionality from origin - [ ] upload - [ ] show - [ ] implement a robust and zero-cost marshalling abstraction - [ ] factor out types and helper functions to own package codeblau.de/taler */ package main import ( "bytes" "crypto/ed25519" "crypto/rsa" "crypto/sha512" "encoding/binary" "encoding/json" "flag" "fmt" "io/ioutil" "log" "math/big" "os" "regexp" "strconv" ) var be = binary.BigEndian type Input 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 } func (ep *EdDSAPublicKey) MarshalJSON() ([]byte, error) { enc, e := crockfordEncode([]byte(*ep)) if e != nil { return nil, e } buf := make([]byte, len(enc)+2) buf[0] = '"' buf[len(buf)-1] = '"' copy(buf[1:], enc) return buf, nil } const CURRENCY_LEN = 12 type Amount struct { Value uint64 `json:"value"` Fraction uint32 `json:"fraction"` Currency [CURRENCY_LEN]byte `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") } copy(a.Currency[:], 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 a.Fraction, e = parseFraction(parts[1]); e != nil { return e } } return nil } // following exchange/src/util/amount.c TALER_string_to_amount() func parseFraction(input []byte) (uint32, error) { const TALER_AMOUNT_FRAC_BASE = 100_000_000 b := uint32(TALER_AMOUNT_FRAC_BASE / 10) var f uint32 for _, c := range input { if b == 0 { return 0, fmt.Errorf("fractional value too small") } if c < '0' || c > '9' { return 0, fmt.Errorf("invalid fractional value") } f += uint32(c-'0') * b b /= 10 } return f, nil } func (a *Amount) Binary() []byte { buf := make([]byte, 8+4+CURRENCY_LEN) be.PutUint64(buf, a.Value) be.PutUint32(buf[8:], a.Fraction) copy(buf[8+4:], a.Currency[:CURRENCY_LEN]) return buf } 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 } // Copy of GNUNET_STRINGS_data_to_string from gnunet/src/util/strings.c func crockfordEncode(in []byte) ([]byte, error) { const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" var ( out []byte rpos, bits, vbit int ) for rpos < len(in) || vbit > 0 { if rpos < len(in) && vbit < 5 { bits = (bits << 8) | int(in[rpos]) // eat 8 more bits rpos++ vbit += 8 } if vbit < 5 { bits <<= (5 - vbit) // zero-padding if vbit != (len(in)*8)%5 { return nil, fmt.Errorf("vbit (%d) != (len(in)*8)%%5 (%d)", vbit, (len(in)*8)%5) } vbit = 5 } vbit -= 5 out = append(out, encTable[(bits>>vbit)&31]) } return out, nil } func (es *EdDSASignature) UnmarshalJSON(in []byte) (e error) { var buf []byte // Decode crockford.base32, the Taler flavour 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 } func (es *EdDSASignature) MarshalJSON() (b []byte, e error) { var buf = make([]byte, len(es.R)+len(es.S)) copy(buf, es.R) copy(buf[len(es.R):], es.S) enc, err := crockfordEncode(buf) if err != nil { return nil, err } b = make([]byte, len(enc)+2) b[0] = '"' b[len(b)-1] = '"' copy(b[1:], enc) return b, nil } func (es *EdDSASignature) BinaryMarshal() []byte { var buf = make([]byte, len(es.R)+len(es.S)) copy(buf, es.R) copy(buf[len(es.R):], es.S) return buf } 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, the Taler flavour 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 (ep *RSAPublicKey) Binary() []byte { nb := ep.N.Bytes() eb := big.NewInt(int64(ep.E)).Bytes() if len(nb) > 2<<16-1 || len(eb) > 2<<16-1 { panic("values too large") } buf := make([]byte, 4+len(nb)+len(eb)) binary.BigEndian.PutUint16(buf, uint16(len(nb))) binary.BigEndian.PutUint16(buf[2:], uint16(len(eb))) copy(buf[4:], nb) copy(buf[4+len(nb):], eb) return buf } func (ep *RSAPublicKey) MarshalJSON() (b []byte, e error) { buf := ep.Binary() enc, err := crockfordEncode(buf) if err != nil { return nil, err } b = make([]byte, len(enc)+2) b[0] = '"' b[len(b)-1] = '"' copy(b[1:], enc) return b, nil } type Output []SignOperation type SignOperation struct { Operation string `json:"operation"` Arguments struct { HDenomPub SHA512Hash `json:"h_denom_pub"` AuditorSig EdDSASignature `json:"auditor_sig"` } `json:"arguments"` } type SHA512Hash [sha512.Size]byte func (h *SHA512Hash) MarshalJSON() ([]byte, error) { enc, err := crockfordEncode((*h)[:]) if err != nil { return nil, fmt.Errorf("error encoding %v: %e\n", h, err) } b := make([]byte, len(enc)+2) b[0] = '"' b[len(b)-1] = '"' copy(b[1:], enc) return b, nil } func Verify(denom *DenomKey, master *EdDSAPublicKey, sig []byte) bool { const TALER_SIGNATURE_MASTER_DENOMINATION_KEY_VALIDITY = 1025 /* data := struct { size uint32 purpose uint32 master EdDSAPublicKey start AbsoluteTime expire_withdraw AbsoluteTime expire_deposit AbsoluteTime expire_legal AbsoluteTime value Amount fee_withdraw Amount fee_deposit Amount fee_refresh Amount fee_refund Amount denom_hash SHA512Hash }{ size: 4 + 4 + ed25519.PublicKeySize + 4*8 + 5*(8+4+len(denom.Value.Currency)) + sha512.Size, purpose: TALER_SIGNATURE_MASTER_DENOMINATION_KEY_VALIDITY, master: input.Arguments.MasterPublicKey, } */ size := 4 + // size 4 + // purpose ed25519.PublicKeySize + // master 4*8 + // start and expire* 5*(8+4+len(denom.Value.Currency)) + // value and fee* sha512.Size // denomHash buf := make([]byte, size) n := 0 be.PutUint32(buf[n:], uint32(size)) n += 4 be.PutUint32(buf[n:], TALER_SIGNATURE_MASTER_DENOMINATION_KEY_VALIDITY) n += 4 copy(buf[n:], *master) n += len(*master) for _, v := range []uint64{ denom.StampStart.TMs, denom.StampExpireWithdraw.TMs, denom.StampExpireDeposit.TMs, denom.StampExpireLegal.TMs, } { be.PutUint64(buf[n:], v*1000) // milli -> micro n += 8 } for _, v := range [][]byte{ denom.Value.Binary(), denom.FeeWithdraw.Binary(), denom.FeeDeposit.Binary(), denom.FeeRefresh.Binary(), denom.FeeRefund.Binary(), } { copy(buf[n:], v) n += len(v) } bin := denom.DenomPub.Binary() hash := sha512.Sum512(bin) copy(buf[n:], hash[:]) return ed25519.Verify(ed25519.PublicKey(*master), buf, sig) } func SignDenom(denom *DenomKey, ahash SHA512Hash, master *EdDSAPublicKey, pk *ed25519.PrivateKey) (SHA512Hash, EdDSASignature) { const TALER_SIGNATURE_AUDITOR_EXCHANGE_KEYS = 1064 /* We write a bigendian encoded version of ExchangeKeyValidityPS. type Purpose uint32 type ExchangeKeyValidityPS struct { size uint32 purpose Purpose auditor_url_hash SHA512Hash master EdDSAPublicKey start AbsoluteTime expireWithdraw AbsoluteTime expireDeposit AbsoluteTime expireLegal AbsoluteTime value Amount feeWithdraw Amount feeDeposit Amount feeRefresh Amount feeRefund Amount denomHash SHA512Hash } */ size := 4 + // size 4 + // purpose sha512.Size + // auditor_url_hash ed25519.PublicKeySize + // master 4*8 + // start and expire* 5*(8+4+len(denom.Value.Currency)) + // value and fee* sha512.Size // denomHash buf := make([]byte, size) n := 0 be.PutUint32(buf[n:], uint32(size)) n += 4 be.PutUint32(buf[n:], TALER_SIGNATURE_AUDITOR_EXCHANGE_KEYS) n += 4 copy(buf[n:], ahash[:]) n += len(ahash) copy(buf[n:], *master) n += len(*master) for _, v := range []uint64{ denom.StampStart.TMs, denom.StampExpireWithdraw.TMs, denom.StampExpireDeposit.TMs, denom.StampExpireLegal.TMs, } { be.PutUint64(buf[n:], v*1000) // milli -> micro n += 8 } for _, v := range [][]byte{ denom.Value.Binary(), denom.FeeWithdraw.Binary(), denom.FeeDeposit.Binary(), denom.FeeRefresh.Binary(), denom.FeeRefund.Binary(), } { copy(buf[n:], v) n += len(v) } bin := denom.DenomPub.Binary() hash := sha512.Sum512(bin) copy(buf[n:], hash[:]) sig := ed25519.Sign(*pk, buf) return hash, EdDSASignature{R: sig[:32], S: sig[32:]} } func Sign(input *Input, url string, pk ed25519.PrivateKey) ([]SignOperation, error) { output := make([]SignOperation, len(input.Arguments.Denoms)) for i, denom := range input.Arguments.Denoms { if !Verify(&denom, &input.Arguments.MasterPublicKey, denom.MasterSig.BinaryMarshal()) { return nil, fmt.Errorf("couldn verify denomination no. %d: %v", i+1, denom.DenomPub) } output[i].Operation = "auditor-sign-denomination-0" hash, sig := SignDenom(&denom, sha512.Sum512(append([]byte(url), 0)), &input.Arguments.MasterPublicKey, &pk) output[i].Arguments.HDenomPub = hash output[i].Arguments.AuditorSig = sig } return output, nil } var ( keyfile = flag.String("key", "auditor.key", "filename of EC25519 private key") url = flag.String("url", "https://auditor.codeblau.de/", "auditor url") ) func main() { flag.Parse() if len(*keyfile) == 0 { log.Println("keyfile needed") return } k, e := ioutil.ReadFile(*keyfile) if e != nil { log.Printf("couldn't read keyfile: %v\n", e) return } pk := ed25519.NewKeyFromSeed(k) input := new(Input) dec := json.NewDecoder(os.Stdin) e = dec.Decode(input) if e != nil { log.Fatal(e) } output, err := Sign(input, *url, pk) if err != nil { log.Fatalf("error signing: %v", err) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") e = enc.Encode(output) if e != nil { log.Fatal(e) } }