651 lines
15 KiB
Go
651 lines
15 KiB
Go
/*
|
|
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)
|
|
}
|
|
|
|
}
|