Commit da96e258 authored by Lukas Matt's avatar Lukas Matt Committed by GitHub

Merge pull request #7 from ganggo/refactor_federation_lib

Refactor signatures
parents 779f47bf bbd8a6a4
language: go
sudo: false
go:
- 1.8.x
- 1.9.x
install:
- go get -t -v ./...
- go get -t -v ./...
script:
- go test -v -race -coverprofile=coverage.txt -covermode=atomic
- go test -v -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)
- bash <(curl -s https://codecov.io/bash)
......@@ -120,18 +120,13 @@ func (a Aes) Decrypt() (ciphertext []byte, err error) {
return ciphertext, nil
}
func (w AesWrapper) Decrypt(serializedKey []byte) (entityXML []byte, err error) {
func (w AesWrapper) Decrypt(privKey *rsa.PrivateKey) (entityXML []byte, err error) {
encryptedAesKey, err := base64.StdEncoding.DecodeString(w.AesKey)
if err != nil {
return
}
privkey, err := ParseRSAPrivKey(serializedKey)
if err != nil {
return
}
decryptedAesKey, err := rsa.DecryptPKCS1v15(rand.Reader, privkey, encryptedAesKey)
decryptedAesKey, err := rsa.DecryptPKCS1v15(rand.Reader, privKey, encryptedAesKey)
if err != nil {
return
}
......
......@@ -37,4 +37,8 @@ const (
// webfinger
WebFingerOstatus = "http://ostatus.org/schema/1.0/subscribe"
WebFingerHcard = "http://microformats.org/profile/hcard"
// signatures
SignatureDelimiter = "."
SignatureAuthorDelimiter = ";"
)
package federation
//
// GangGo Diaspora Federation Library
// Copyright (C) 2017 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"crypto"
"crypto/rsa"
"crypto/x509"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/pem"
"errors"
"reflect"
"strings"
)
func ParseRSAPubKey(decodedKey []byte) (pubkey *rsa.PublicKey, err error) {
block, _ := pem.Decode(decodedKey)
if block == nil {
err = errors.New("Decode public key block is nil!")
return
}
data, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return
}
switch data := data.(type) {
case *rsa.PublicKey:
pubkey = data
default:
err = errors.New("Wasn't able to parse the public key!")
}
return
}
func ParseRSAPrivKey(decodedKey []byte) (privkey *rsa.PrivateKey, err error) {
block, _ := pem.Decode(decodedKey)
if block == nil {
err = errors.New("Decode private key block is nil!")
return
}
privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return
}
return
}
func AuthorSignature(data interface{}, order string, privKey []byte) (string, error) {
var text string
var r = reflect.TypeOf(data)
var v = reflect.ValueOf(data)
for _, o := range strings.Split(order, " ") {
for i := 0; i < r.NumField(); i++ {
tagList := strings.Split(r.Field(i).Tag.Get("xml"), ",")
if len(tagList) <= 0 {
panic("xml struct always requires an xml tag for signatures")
}
tag := tagList[0] // the first element is always the xml name
if tag == o {
value := v.Field(i).Interface()
switch v := value.(type) {
case Time:
text += v.Format(TIME_FORMAT) + ";"
case string:
text += v + ";"
case bool:
positive := "false"
if v {
positive = "true"
}
text += positive + ";"
default:
err := errors.New("Unknown type in AuthorSignature that will break federation!")
return text, err
}
}
}
}
if len(text) <= 0 {
return text, errors.New("AuthorSignature text is empty!")
}
// trim last semicolon
text = text[:len(text)-1]
return Sign(text, privKey)
}
func (envelope *MagicEnvelopeMarshal) Sign(privKey string) (err error) {
type64 := base64.StdEncoding.EncodeToString(
[]byte(envelope.Data.Type),
)
encoding64 := base64.StdEncoding.EncodeToString(
[]byte(envelope.Encoding),
)
alg64 := base64.StdEncoding.EncodeToString(
[]byte(envelope.Alg),
)
text := envelope.Data.Data + "." + type64 +
"." + encoding64 + "." + alg64
(*envelope).Sig.Sig, err = Sign(text, []byte(privKey))
return
}
func Sign(text string, privKey []byte) (sig string, err error) {
privkey, err := ParseRSAPrivKey(privKey)
if err != nil {
return "", err
}
h := sha256.New()
h.Write([]byte(text))
digest := h.Sum(nil)
rng := rand.Reader
sigInByte, err := rsa.SignPKCS1v15(rng, privkey, crypto.SHA256, digest[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(sigInByte), nil
}
......@@ -19,26 +19,26 @@ package federation
import (
"errors"
"encoding/xml"
"github.com/Zauberstuhl/go-xml"
"encoding/base64"
"time"
)
type Message struct {
XMLName xml.Name `xml:"env"`
XMLName xml.Name `xml:"me:env"`
Me string `xml:"me,attr"`
Data struct {
XMLName xml.Name `xml:"data"`
XMLName xml.Name `xml:"me:data"`
Type string `xml:"type,attr"`
Data string `xml:",chardata"`
}
Encoding string `xml:"encoding"`
Alg string `xml:"alg"`
Encoding string `xml:"me:encoding"`
Alg string `xml:"me:alg"`
Sig struct {
XMLName xml.Name `xml:"sig"`
XMLName xml.Name `xml:"me:sig"`
Sig string `xml:",chardata"`
KeyId string `xml:"key_id,attr,omitempty"`
}
Entity Entity `xml:"-"`
}
type Entity struct {
......@@ -54,6 +54,15 @@ type Time struct {
time.Time
}
func (m Message) SignatureText() []string {
return []string{
m.Data.Data,
base64.StdEncoding.EncodeToString([]byte(m.Data.Type)),
base64.StdEncoding.EncodeToString([]byte(m.Encoding)),
base64.StdEncoding.EncodeToString([]byte(m.Alg)),
}
}
func (t *Time) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
e.EncodeElement(t.Format(TIME_FORMAT), start)
return nil
......
......@@ -19,7 +19,7 @@ package federation
import (
"testing"
"encoding/xml"
"github.com/Zauberstuhl/go-xml"
)
func TestEntitiesUnmarshalXML(t *testing.T) {
......
......@@ -17,7 +17,7 @@ package federation
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "encoding/xml"
import "github.com/Zauberstuhl/go-xml"
type EntityComment struct {
XMLName xml.Name `xml:"comment"`
......@@ -29,16 +29,12 @@ type EntityComment struct {
AuthorSignature string `xml:"author_signature"`
}
func (e *EntityComment) SignatureOrder() string {
return "author created_at guid parent_guid text"
}
func (e *EntityComment) AppendSignature(privKey []byte, order string) error {
signature, err := AuthorSignature(*e, order, privKey)
if err != nil {
return err
func (e EntityComment) SignatureText() []string {
return []string{
e.Author,
e.CreatedAt.Format(TIME_FORMAT),
e.Guid,
e.ParentGuid,
e.Text,
}
(*e).AuthorSignature = signature
return nil
}
......@@ -22,15 +22,6 @@ import (
"time"
)
func TestCommentSignatureOrder(t *testing.T) {
var comment EntityComment
expected := "author created_at guid parent_guid text"
if expected != comment.SignatureOrder() {
t.Errorf("Expected to be %s, got %s", expected, comment.SignatureOrder())
}
}
func TestCommentAppendSignature(t *testing.T) {
comment := EntityComment{
Author: "author@localhost",
......@@ -44,17 +35,18 @@ func TestCommentAppendSignature(t *testing.T) {
t.Errorf("Expected to be empty, got %s", comment.AuthorSignature)
}
err := comment.AppendSignature(TEST_PRIV_KEY, comment.SignatureOrder())
privKey, err := ParseRSAPrivateKey(TEST_PRIV_KEY)
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
}
if comment.AuthorSignature == "" {
t.Errorf("Expected signature, was empty")
}
err = comment.AppendSignature(TEST_PRIV_KEY, comment.SignatureOrder())
var signature Signature
err = signature.New(comment).Sign(privKey, &(comment.AuthorSignature))
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
}
if comment.AuthorSignature == "" {
t.Errorf("Expected signature, got empty string")
}
}
......@@ -17,7 +17,7 @@ package federation
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "encoding/xml"
import "github.com/Zauberstuhl/go-xml"
type EntityContact struct {
XMLName xml.Name `xml:"contact"`
......
......@@ -17,7 +17,7 @@ package federation
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "encoding/xml"
import "github.com/Zauberstuhl/go-xml"
type EntityLike struct {
XMLName xml.Name `xml:"like"`
......@@ -27,18 +27,21 @@ type EntityLike struct {
ParentType string `xml:"parent_type"`
Author string `xml:"author"`
AuthorSignature string `xml:"author_signature"`
}
func (e *EntityLike) SignatureOrder() string {
return "positive guid parent_guid parent_type author"
// store relayable signature order
SignatureOrder string `xml:"-"`
}
func (e *EntityLike) AppendSignature(privKey []byte, order string) error {
signature, err := AuthorSignature(*e, order, privKey)
if err != nil {
return err
func (e EntityLike) SignatureText() []string {
positive := "false"
if e.Positive {
positive = "true"
}
return []string{
positive,
e.Guid,
e.ParentGuid,
e.ParentType,
e.Author,
}
(*e).AuthorSignature = signature
return nil
}
......@@ -19,16 +19,6 @@ package federation
import "testing"
func TestLikeSignatureOrder(t *testing.T) {
var like EntityLike
expected := "positive guid parent_guid parent_type author"
if expected != like.SignatureOrder() {
t.Errorf("Expected to be %s, got %s", expected, like.SignatureOrder())
}
}
func TestLikeAppendSignature(t *testing.T) {
like := EntityLike{
Positive: true,
......@@ -38,21 +28,18 @@ func TestLikeAppendSignature(t *testing.T) {
Author: "author@localhost",
}
if like.AuthorSignature != "" {
t.Errorf("Expected to be empty, got %s", like.AuthorSignature)
privKey, err := ParseRSAPrivateKey(TEST_PRIV_KEY)
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
}
err := like.AppendSignature(TEST_PRIV_KEY, like.SignatureOrder())
var signature Signature
err = signature.New(like).Sign(privKey, &(like.AuthorSignature))
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
}
if like.AuthorSignature == "" {
t.Errorf("Expected signature, was empty")
}
err = like.AppendSignature(TEST_PRIV_KEY, like.SignatureOrder())
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
t.Errorf("Expected signature, got empty string")
}
}
......@@ -17,7 +17,7 @@ package federation
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "encoding/xml"
import "github.com/Zauberstuhl/go-xml"
type EntityStatusMessage struct {
XMLName xml.Name `xml:"status_message"`
......
......@@ -17,7 +17,7 @@ package federation
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "encoding/xml"
import "github.com/Zauberstuhl/go-xml"
type EntityRetraction struct {
XMLName xml.Name `xml:"retraction"`
......
......@@ -19,107 +19,33 @@ package federation
import (
"regexp"
"strings"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
)
func FetchEntityOrder(entityXML string) (order string) {
re := regexp.MustCompile(`<([^/<>]+?)>.+?</[^/<>]+?>`)
elements := re.FindAllStringSubmatch(entityXML, -1)
for _, element := range elements {
if len(element) == 2 {
switch element[1] {
case "author_signature":
case "parent_author_signature":
default:
order += element[1] + " "
}
}
func ParseRSAPublicKey(decodedKey []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(decodedKey)
if block == nil {
return nil, errors.New("Decode public key block is nil")
}
if len(order) <= 0 {
logger.Warn("Entity order is empty")
return
}
return order[:len(order)-1] // trim space
}
// This is a workaround for sorting xml elements. Diaspora requires
// a specific order otherwise the signature check will fail and
// ignore the entity. This should be a TODO since we could implement
// this kind of logic in a custom xml marshaller
func SortByEntityOrder(order string, entity []byte) (sorted []byte) {
// if we do not require sorting skip it
if order == "" {
return entity
}
// remove all newline character
entity = []byte(strings.Replace(string(entity), "\n", "", -1))
entity = []byte(strings.Replace(string(entity), "\r", "", -1))
var lines []string
var linesOffset int
var closingTag bool
var entityLen = len(entity)
for index, c := range entity {
offset := index + 1
// abort on last character
if offset >= entityLen {
lines = append(lines, string(entity[linesOffset:]))
break
}
// check on "><" open xml tags
if c == 0x003e && entity[offset] == 0x003c {
lines = append(lines, string(entity[linesOffset:offset]))
linesOffset = offset
}
// set the closing tag to true if "/" occurs
if c == 0x002f {
closingTag = true
}
// append the whole xml element after ">" if "/" is true
if c == 0x003e && closingTag {
lines = append(lines, string(entity[linesOffset:offset]))
linesOffset = offset
closingTag = false
}
data, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
var start bool = true
var orderArr = strings.Split(order, " ")
var sortedXmlElements string
// sort the elements in the prefered order
for _, o := range orderArr {
re := regexp.MustCompile("<"+o+">(.+?)</"+o+">")
elements := re.FindAllStringSubmatch(string(entity), 1)
if len(elements) > 0 && len(elements[0]) > 0 {
sortedXmlElements += elements[0][0]
}
if pubKey, ok := data.(*rsa.PublicKey); ok {
return pubKey, nil
}
return nil, errors.New("Wasn't able to parse the public key!")
}
// replace only the elements we have to sort
// with the new sortedXmlElements
for _, line := range lines {
var orderMatch bool = false
for _, o := range orderArr {
re := regexp.MustCompile("<"+o+">(.+?)</"+o+">")
if re.Find([]byte(line)) != nil {
orderMatch = true
break
}
}
if !orderMatch {
sorted = append(sorted, []byte(line)...)
} else {
if start {
sorted = append(sorted, []byte(sortedXmlElements)...)
}
start = false
}
func ParseRSAPrivateKey(decodedKey []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(decodedKey)
if block == nil {
return nil, errors.New("Decode private key block is nil!")
}
return
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
func ParseWebfingerHandle(handle string) (string, error) {
......
......@@ -18,8 +18,8 @@ package federation
//
import (
"crypto/rsa"
"testing"
"bytes"
)
var TEST_AUTHOR = `diaspora_2nd@localhost:3001`
......@@ -69,38 +69,55 @@ zQKne2ejiu9e5cDD5WyVusjeRstj/+9bDlOrQ8X4eh6vmjvd+98B/ZWFCCEkTH5m
DX3H15lu+GelrDpYLThXjnkCAwEAAQ==
-----END PUBLIC KEY-----`)
func TestFetchEntityOrder(t *testing.T) {
var order = "author guid"
func TestParseRSAPublicKey(t *testing.T) {
var (
err error
pub interface{}
)
extractedOrder := FetchEntityOrder(
`<author></author><author_signature>` +
`</author_signature><guid></guid>` +
`<parent_author_signature></parent_author_signature>`)
if extractedOrder != order {
t.Errorf("Expected to be %s, got %s", order, extractedOrder)
pub, err = ParseRSAPublicKey(TEST_PUB_KEY)
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
}
if data, ok := pub.(*rsa.PublicKey); !ok {
t.Errorf("Expected to be '*rsa.PublicKey', got %v", data)
}
_, err = ParseRSAPublicKey([]byte(""))
if err == nil {
t.Errorf("Expected an error, got nil")
}
extractedOrder = FetchEntityOrder(
`<author_signature></author_signature>` +
`<parent_author_signature></parent_author_signature>`)
if extractedOrder != "" {
t.Errorf("Expected to be empty, got %s", extractedOrder)
_, err = ParseRSAPublicKey(TEST_PRIV_KEY)
if err == nil {
t.Errorf("Expected an error, got nil")
}
}
func TestSortByEntityOrder(t *testing.T) {
entity := []byte(`<author>test</author><guid>1234</guid>`)
expectedEntity := []byte(`<guid>1234</guid><author>test</author>`)
func TestParseRSAPrivateKey(t *testing.T) {
var (
err error
priv interface{}
)
sorted := SortByEntityOrder("guid author", entity)
priv, err = ParseRSAPrivateKey(TEST_PRIV_KEY)
if err != nil {
t.Errorf("Some error occured while parsing: %v", err)
}
if bytes.Compare(sorted, expectedEntity) != 0 {
t.Errorf("Expected to be %s, got %s", expectedEntity, sorted)
if data, ok := priv.(*rsa.PrivateKey); !ok {
t.Errorf("Expected to be '*rsa.PrivateKey', got %v", data)
}
sorted = SortByEntityOrder("", entity)
if bytes.Compare(sorted, entity) != 0 {
t.Errorf("Expected to be %s, got %s", entity, sorted)
_, err = ParseRSAPrivateKey([]byte(""))
if err == nil {
t.Errorf("Expected an error, got nil")
}
_, err = ParseRSAPrivateKey(TEST_PUB_KEY)
if err == nil {
t.Errorf("Expected an error, got nil")
}
}
......
......@@ -21,7 +21,7 @@ import (
"time"
"net/http"
"encoding/json"
"encoding/xml"
"github.com/Zauberstuhl/go-xml"
"errors"
"io"
"strings"
......
......@@ -22,7 +22,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"encoding/xml"
"github.com/Zauberstuhl/go-xml"
"io/ioutil"
)
......
......@@ -20,12 +20,12 @@ package federation
import (
"fmt"
"runtime"
"regexp"
"os"
"log"
)
const (
LOG_C_TUR = "\033[0;36m"
LOG_C_RED = "\033[31m"
LOG_C_YELLOW = "\033[33m"
LOG_C_RESET = "\033[0m"
......@@ -37,18 +37,16 @@ var (
)
func init() {
pc := make([]uintptr, 10) // at least 1 entry needed
pc := make([]uintptr, 1)
runtime.Callers(3, pc)
f := runtime.FuncForPC(pc[0])
file, line := f.FileLine(pc[0])
regex, _ := regexp.Compile(`\/([^\/]+?\.go)`)
result := regex.FindAllStringSubmatch(file, -1)
if len(result) == 1 {
file = result[0][1]
}
defaultPrefix = fmt.Sprintf("%s:%d %s ", file, line, f.Name())
logger = Logger{log.New(os.Stdout, defaultPrefix, log.Lshortfile)}
logger = Logger{
log.New(os.Stdout, defaultPrefix, log.Lshortfile),
LOG_C_TUR,
}
}
type LogWriter interface {
......@@ -57,20 +55,28 @@ type LogWriter interface {
type Logger struct{
LogWriter
Prefix string
}
func SetLogger(writer LogWriter) {
logger = Logger{writer}
logger = Logger{writer, LOG_C_TUR}
}
func (l Logger) Info(values... interface{}) {
l.Println(values...)
values = append(values, []interface{}{""})
copy(values[1:], values[0:])
values[0] = l.Prefix
values = append(values, LOG_C_RESET)
l.Println(values)
}
func (l Logger) Error(values... interface{}) {
l.Info(LOG_C_RED, values, LOG_C_RESET)
l.Prefix = LOG_C_RED
l.Info(values)
}
func (l Logger) Warn(values... interface{}) {
l.Info(LOG_C_YELLOW, values, LOG_C_RESET)
l.Prefix = LOG_C_YELLOW