package document // An epoint document is a clear signed utf-8 text of key-value pairs // according to OpenPGP RFC 4880. The body contains a content-type // MIME header so it can be used in OpenPGP/MIME RFC 3156 emails with // the signature detached. The format of the key-value pairs are // similar to MIME header fields. // // TODO: allow visually aligned field values // TODO: handling repeated fields // TODO: representation of a list (references) // // Example: // // -----BEGIN PGP SIGNED MESSAGE----- // Hash: SHA1 // // Content-Type: text/plain.epoint.cert; charset=utf-8 // // Key: Value1 // Another-Key: Value2 // -----BEGIN PGP SIGNATURE----- // pgp signature // -----END PGP SIGNATURE----- import ( "bytes" "strconv" ) const ClearSignedHeader = "-----BEGIN PGP SIGNED MESSAGE-----\n" // Non-standard MIME subtype see RFC 2045 and RFC 2046 // TODO: verify that Content-Transfer-Encoding is not needed // TODO: text/epoint.cert would be shorter var ContentType = map[string]string{ "cert": "text/plain.epoint.cert; charset=utf-8", "draft": "text/plain.epoint.draft; charset=utf-8", } // OpenPGP signed cleartext document representation type ClearSigned struct { Hash string // Signed text (no dash escape, no trailing space) Body []byte // Armored detached text signature of the Body ArmoredSignature []byte } // Draft document represents an obligation transfer order type Draft struct { Drawer string // ID of the payer (signer of the document) Beneficiary string // ID of the payee Amount int64 // amount transfered // TODO: issuer keys is enough? Denomination string // TODO: maturity date is enough if the only constraint is <= now IssueDate int64 MaturityDate int64 // Draft is bounced before this date // TODO: implement limits Notes string // Arbitrary text notes of the drawer // TODO: hack to make signed draft unique (not required for DSA) Nonce string // unique number // TODO: server ID might change, do we need it? Server string // ID of the server (drawee?) //TODO: naming: drawee vs issuer Drawee string // ID of the obligation issuer // TODO: reference cert ids in the draft // useful if more strict date of issue information is needed //References []string } // TODO: cert references: fpr+serial, fpr+certID // Certification of obligation after a transfer // References previous certificate (if any) // and the transfer related other documents type Cert struct { Holder string // ID of the creditor Serial uint32 // serial number, number of certs of the holder Date int64 // date of issue Balance int64 // current obligation value Denomination string Issuer string // ID of the obligation issuer (drawee?) LastDebitSerial uint32 // serial of the last draft cert or 0 LastCreditSerial uint32 // serial of the last credit cert or 0 // TODO: move to References? LastCert string // ID of the previous cert if any // TODO: determine cert type from diff value only? // (>0: credit cert, <0: debit cert, ==0: special) Difference int64 // difference from previous balance // TODO: enough on the debit side Draft string // draft ID related to the transfer // TODO: credit side, redundant references Drawer string // ID of the drawer in the transaction DrawerSerial uint32 // serial of the drawer's related debit cert DrawerCert string // ID of the drawer's related debit cert // TODO: fingerprint? References []string // cert IDs for timestamping the system } func DecodeClearSigned(s []byte) (c *ClearSigned, err error) { hash, body, sig := split(s) if len(sig) == 0 { // TODO: split errors return } c = &ClearSigned{string(hash), trimspace(dashunesc(body)), sig} return } func EncodeClearSigned(c *ClearSigned) (data []byte, err error) { s := ClearSignedHeader if c.Hash != "" { s += "Hash: " + c.Hash + "\n" } // TODO: check if space was trimmed from body before signature s += "\n" s += string(dashesc(c.Body)) s += "\n" s += string(c.ArmoredSignature) data = []byte(s) return } func ParseFields(s []byte) (fields map[string]string, rest []byte, err error) { // TODO: error fields = make(map[string]string) rest = s for len(rest) > 0 { var line []byte line, rest = getLine(rest) // empty line after the parsed fields (consumed) if len(line) == 0 { break } i := bytes.Index(line, []byte(": ")) // TODO: long lines can be broken up in MIME if i < 0 { return nil, nil, nil } // TODO: repeated fields fields[string(line[:i])] = string(line[i+2:]) } return } func ParseBody(s []byte) (t string, fields map[string]string, err error) { // parse content type header first mime, s, err := ParseFields(s) if len(mime) != 1 { return } for k, v := range ContentType { if mime["Content-Type"] == v { t = k fields, s, err = ParseFields(s) if len(s) > 0 { fields = nil break } return } } // TODO: error return } /* rendering with reflect func render(d interface{}) (s []byte, err error) { a := []string{} v := reflect.ValueOf(d) t := v.Type() n := v.NumField() for i := 0; i < n; i++ { f := t.Field(i) fv := v.Field(i) fs := "" switch fv.Type() { case reflect.String: fs = fv.String() // TODO: quote, esc (\n..) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fs = strconv.Itoa64(fv.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: fs = strconv.Uitoa64(fv.Uint()) default: return // TODO: error } a = append(a, f.Name, ": ", fs, "\n") } s = strings.Join(a, "") return } */ func checkID(s string) (string, error) { return s, nil } func ParseDraft(s []byte) (draft *Draft, err error) { t, fields, err := ParseBody(s) if err != nil { return } if t != "draft" { return } draftFields := []string{ "Drawer", "Beneficiary", "Amount", "Denomination", "IssueDate", "MaturityDate", "Notes", "Nonce", "Server", "Drawee", } if len(fields) != len(draftFields) { return } for _, f := range draftFields { _, ok := fields[f] if !ok { return } } draft = new(Draft) draft.Drawer, _ = checkID(fields["Drawer"]) draft.Beneficiary, _ = checkID(fields["Beneficiary"]) draft.Amount, _ = strconv.Atoi64(fields["Amount"]) draft.Denomination = fields["Denomination"] draft.IssueDate, _ = strconv.Atoi64(fields["IssueDate"]) draft.MaturityDate, _ = strconv.Atoi64(fields["MaturityDate"]) draft.Notes = fields["Notes"] draft.Nonce = fields["Nonce"] draft.Server, _ = checkID(fields["Server"]) draft.Drawee, _ = checkID(fields["Drawee"]) // more checks.. return } func RenderDraft(draft *Draft) (data []byte, err error) { s := "Content-Type: " + ContentType["draft"] + "\n" s += "\n" s += "Drawer: " + draft.Drawer + "\n" s += "Beneficiary: " + draft.Beneficiary + "\n" s += "Amount: " + strconv.Itoa64(draft.Amount) + "\n" s += "Denomination: " + draft.Denomination + "\n" s += "IssueDate: " + strconv.Itoa64(draft.IssueDate) + "\n" s += "MaturityDate: " + strconv.Itoa64(draft.MaturityDate) + "\n" s += "Notes: " + draft.Notes + "\n" s += "Nonce: " + draft.Nonce + "\n" s += "Server: " + draft.Server + "\n" s += "Drawee: " + draft.Drawee + "\n" data = []byte(s) return } func ParseCert(s []byte) (cert Cert, err error) { return } func RenderCert(cert Cert) (s []byte, err error) { return } func splitline(s []byte) (line, rest []byte) { i := bytes.IndexByte(s, '\n') if i < 0 { line = s } else { rest = s[i+1:] if i > 0 && s[i-1] == '\r' { i-- } line = s[:i] } return } func getLine(data []byte) (line, rest []byte) { i := bytes.Index(data, []byte{'\n'}) j := i + 1 if i < 0 { i = len(data) j = i } else if i > 0 && data[i-1] == '\r' { i-- } return data[:i], data[j:] } func trimspace(s []byte) []byte { a := bytes.Split(s, []byte("\n")) for i := range a { a[i] = bytes.TrimRight(a[i], " \t\r") } return bytes.Join(a, []byte("\n")) } func dashesc(s []byte) []byte { r := bytes.Replace(s, []byte("\n-"), []byte("\n- -"), -1) if len(r) > 0 && r[0] == '-' { r = append([]byte("- "), r...) } return r } func dashunesc(s []byte) []byte { r := bytes.Replace(s, []byte("\n- "), []byte("\n"), -1) if len(r) >= 2 && r[0] == '-' && r[1] == ' ' { r = r[2:] } return r } // RFC 4880 is unclear about multiple Hash header semantics, section 7. says // "One or more "Hash" Armor Headers" // then // "If more than one message digest is used in the signature, the "Hash" // armor header contains a comma-delimited list of used message digests." // in section 6.2. // "there is no limit to the length of Armor Headers. Care should // be taken that the Armor Headers are short enough to survive // transport. One way to do this is to repeat an Armor Header key // multiple times with different values for each so that no one line is // overly long." // we accept a single Hash header with a list of hash algorithms for now // but use the one specified by the signature func split(s []byte) (hash, body, sig []byte) { if !bytes.HasPrefix(s, []byte(ClearSignedHeader)) { return } s = s[len(ClearSignedHeader):] // only allow a single Hash: header if bytes.HasPrefix(s, []byte("Hash: ")) { s = s[len("Hash: "):] hash, s = getLine(s) } // skip empty line empty, s := getLine(s) if len(empty) != 0 { return } i := bytes.Index(s, []byte("\n-----BEGIN")) if i < 0 { return } body, sig = s[:i], s[i+1:] if i > 0 && body[i-1] == '\r' { body = body[:i-1] } return }