3 // An epoint document is a clear signed utf-8 text of key-value pairs
4 // according to OpenPGP RFC 4880. The body contains a content-type
5 // MIME header so it can be used in OpenPGP/MIME RFC 3156 emails with
6 // the signature detached. The format of the key-value pairs are
7 // similar to MIME header fields.
9 // TODO: allow visually aligned field values
10 // TODO: handling repeated fields
11 // TODO: representation of a list (references)
15 // -----BEGIN PGP SIGNED MESSAGE-----
18 // Content-Type: text/plain.epoint.cert; charset=utf-8
21 // Another-Key: Value2
22 // -----BEGIN PGP SIGNATURE-----
24 // -----END PGP SIGNATURE-----
31 const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n"
33 // Non-standard MIME subtype see RFC 2045 and RFC 2046
34 // TODO: verify that Content-Transfer-Encoding is not needed
35 // TODO: text/epoint.cert would be shorter
36 var ContentType = map[string]string{
37 "cert": "text/plain.epoint.cert; charset=utf-8",
38 "draft": "text/plain.epoint.draft; charset=utf-8",
41 // OpenPGP signed cleartext document representation
42 type ClearSigned struct {
44 // Signed text (no dash escape, no trailing space)
46 // Armored detached text signature of the Body
47 ArmoredSignature []byte
50 // Draft document represents an obligation transfer order
52 Drawer string // ID of the payer (signer of the document)
53 Beneficiary string // ID of the payee
54 Amount int64 // amount transfered
55 // TODO: issuer keys is enough?
57 // TODO: maturity date is enough if the only constraint is <= now
59 MaturityDate int64 // Draft is bounced before this date
60 // TODO: implement limits
61 Notes string // Arbitrary text notes of the drawer
62 // TODO: hack to make signed draft unique (not required for DSA)
63 Nonce string // unique number
64 // TODO: server ID might change, do we need it?
65 Server string // ID of the server (drawee?)
66 //TODO: naming: drawee vs issuer
67 Drawee string // ID of the obligation issuer
68 // TODO: reference cert ids in the draft
69 // useful if more strict date of issue information is needed
73 // TODO: cert references: fpr+serial, fpr+certID
74 // Certification of obligation after a transfer
75 // References previous certificate (if any)
76 // and the transfer related other documents
78 Holder string // ID of the creditor
79 Serial uint32 // serial number, number of certs of the holder
80 Date int64 // date of issue
81 Balance int64 // current obligation value
83 Issuer string // ID of the obligation issuer (drawee?)
84 LastDebitSerial uint32 // serial of the last draft cert or 0
85 LastCreditSerial uint32 // serial of the last credit cert or 0
86 // TODO: move to References?
87 LastCert string // ID of the previous cert if any
88 // TODO: determine cert type from diff value only?
89 // (>0: credit cert, <0: debit cert, ==0: special)
90 Difference int64 // difference from previous balance
91 // TODO: enough on the debit side
92 Draft string // draft ID related to the transfer
93 // TODO: credit side, redundant references
94 Drawer string // ID of the drawer in the transaction
95 DrawerSerial uint32 // serial of the drawer's related debit cert
96 DrawerCert string // ID of the drawer's related debit cert
98 References []string // cert IDs for timestamping the system
101 func DecodeClearSigned(s []byte) (c *ClearSigned, err error) {
102 hash, body, sig := split(s)
104 // TODO: split errors
107 c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig}
111 func EncodeClearSigned(c *ClearSigned) (data []byte, err error) {
112 s := ClearSignedHeader
114 s += "Hash: " + c.Hash + "\n"
116 // TODO: check if space was trimmed from body before signature
118 s += string(dashesc(c.Body))
120 s += string(c.ArmoredSignature)
125 func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) {
127 fields = make(map[string]string)
131 line, rest = getLine(rest)
132 // empty line after the parsed fields (consumed)
136 i := bytes.Index(line, []byte(": "))
137 // TODO: long lines can be broken up in MIME
141 // TODO: repeated fields
142 fields[string(line[:i])] = string(line[i+2:])
147 func ParseBody(s []byte) (t string, fields map[string]string, err error) {
148 // parse content type header first
149 mime, s, err := ParseFields(s)
153 for k, v := range ContentType {
154 if mime["Content-Type"] == v {
156 fields, s, err = ParseFields(s)
168 /* rendering with reflect
169 func render(d interface{}) (s []byte, err error) {
171 v := reflect.ValueOf(d)
174 for i := 0; i < n; i++ {
180 fs = fv.String() // TODO: quote, esc (\n..)
181 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
182 fs = strconv.Itoa64(fv.Int())
183 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
184 fs = strconv.Uitoa64(fv.Uint())
186 return // TODO: error
188 a = append(a, f.Name, ": ", fs, "\n")
190 s = strings.Join(a, "")
195 func checkID(s string) (string, error) {
199 func ParseDraft(s []byte) (draft *Draft, err error) {
200 t, fields, err := ParseBody(s)
208 draftFields := []string{
220 if len(fields) != len(draftFields) {
223 for _, f := range draftFields {
231 draft.Drawer, _ = checkID(fields["Drawer"])
232 draft.Beneficiary, _ = checkID(fields["Beneficiary"])
233 draft.Amount, _ = strconv.Atoi64(fields["Amount"])
234 draft.Denomination = fields["Denomination"]
235 draft.IssueDate, _ = strconv.Atoi64(fields["IssueDate"])
236 draft.MaturityDate, _ = strconv.Atoi64(fields["MaturityDate"])
237 draft.Notes = fields["Notes"]
238 draft.Nonce = fields["Nonce"]
239 draft.Server, _ = checkID(fields["Server"])
240 draft.Drawee, _ = checkID(fields["Drawee"])
247 func RenderDraft(draft *Draft) (data []byte, err error) {
248 s := "Content-Type: " + ContentType["draft"] + "\n"
250 s += "Drawer: " + draft.Drawer + "\n"
251 s += "Beneficiary: " + draft.Beneficiary + "\n"
252 s += "Amount: " + strconv.Itoa64(draft.Amount) + "\n"
253 s += "Denomination: " + draft.Denomination + "\n"
254 s += "IssueDate: " + strconv.Itoa64(draft.IssueDate) + "\n"
255 s += "MaturityDate: " + strconv.Itoa64(draft.MaturityDate) + "\n"
256 s += "Notes: " + draft.Notes + "\n"
257 s += "Nonce: " + draft.Nonce + "\n"
258 s += "Server: " + draft.Server + "\n"
259 s += "Drawee: " + draft.Drawee + "\n"
264 func ParseCert(s []byte) (cert Cert, err error) {
268 func RenderCert(cert Cert) (s []byte, err error) {
272 func splitline(s []byte) (line, rest []byte) {
273 i := bytes.IndexByte(s, '\n')
278 if i > 0 && s[i-1] == '\r' {
286 func getLine(data []byte) (line, rest []byte) {
287 i := bytes.Index(data, []byte{'\n'})
292 } else if i > 0 && data[i-1] == '\r' {
295 return data[:i], data[j:]
298 func trimspace(s []byte) []byte {
299 a := bytes.Split(s, []byte("\n"))
301 a[i] = bytes.TrimRight(a[i], " \t\r")
303 return bytes.Join(a, []byte("\n"))
306 func dashesc(s []byte) []byte {
307 r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1)
308 if len(r) > 0 && r[0] == '-' {
309 r = append([]byte("- "), r...)
314 func dashunesc(s []byte) []byte {
315 r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1)
316 if len(r) >= 2 && r[0] == '-' && r[1] == ' ' {
322 // RFC 4880 is unclear about multiple Hash header semantics, section 7. says
323 // "One or more "Hash" Armor Headers"
325 // "If more than one message digest is used in the signature, the "Hash"
326 // armor header contains a comma-delimited list of used message digests."
328 // "there is no limit to the length of Armor Headers. Care should
329 // be taken that the Armor Headers are short enough to survive
330 // transport. One way to do this is to repeat an Armor Header key
331 // multiple times with different values for each so that no one line is
333 // we accept a single Hash header with a list of hash algorithms for now
334 // but use the one specified by the signature
336 func split(s []byte) (hash, body, sig []byte) {
337 if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) {
340 s = s[len(ClearSignedHeader):]
341 // only allow a single Hash: header
342 if bytes.HasPrefix(s, []byte("Hash: ")) {
343 s = s[len("Hash: "):]
347 empty, s := getLine(s)
351 i := bytes.Index(s, []byte("\n-----BEGIN"))
355 body, sig = s[:i], s[i+1:]
356 if i > 0 && body[i-1] == '\r' {