From e9a9c20d52647094355df04c441a1783595a7853 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.io>
Date: Tue, 28 Sep 2021 15:19:22 -0400
Subject: [PATCH] Create pub/priv keypair for federation (#17071)

* add logic for creating pub/priv keypair for federation

* Apply suggestions from code review

Co-authored-by: delvh <dev.lh@web.de>

* make fmt

* Update modules/activitypub/keypair.go

Co-authored-by: delvh <dev.lh@web.de>

* add tests

* fix revert

* more tests

* Apply suggestions from code review

Co-authored-by: delvh <dev.lh@web.de>

* make fmt

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: 6543 <6543@obermui.de>
---
 modules/activitypub/keypair.go      | 48 ++++++++++++++++++++++
 modules/activitypub/keypair_test.go | 63 +++++++++++++++++++++++++++++
 2 files changed, 111 insertions(+)
 create mode 100644 modules/activitypub/keypair.go
 create mode 100644 modules/activitypub/keypair_test.go

diff --git a/modules/activitypub/keypair.go b/modules/activitypub/keypair.go
new file mode 100644
index 000000000..fe6aba905
--- /dev/null
+++ b/modules/activitypub/keypair.go
@@ -0,0 +1,48 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package activitypub
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+)
+
+const rsaBits = 2048
+
+// GenerateKeyPair generates a public and private keypair for signing actions by users for activitypub purposes
+func GenerateKeyPair() (string, string, error) {
+	priv, _ := rsa.GenerateKey(rand.Reader, rsaBits)
+	privPem, err := pemBlockForPriv(priv)
+	if err != nil {
+		return "", "", err
+	}
+	pubPem, err := pemBlockForPub(&priv.PublicKey)
+	if err != nil {
+		return "", "", err
+	}
+	return privPem, pubPem, nil
+}
+
+func pemBlockForPriv(priv *rsa.PrivateKey) (string, error) {
+	privBytes := pem.EncodeToMemory(&pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(priv),
+	})
+	return string(privBytes), nil
+}
+
+func pemBlockForPub(pub *rsa.PublicKey) (string, error) {
+	pubASN1, err := x509.MarshalPKIXPublicKey(pub)
+	if err != nil {
+		return "", err
+	}
+	pubBytes := pem.EncodeToMemory(&pem.Block{
+		Type:  "PUBLIC KEY",
+		Bytes: pubASN1,
+	})
+	return string(pubBytes), nil
+}
diff --git a/modules/activitypub/keypair_test.go b/modules/activitypub/keypair_test.go
new file mode 100644
index 000000000..5d876937b
--- /dev/null
+++ b/modules/activitypub/keypair_test.go
@@ -0,0 +1,63 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package activitypub
+
+import (
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/pem"
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestKeygen(t *testing.T) {
+	priv, pub, err := GenerateKeyPair()
+	assert.NoError(t, err)
+
+	assert.NotEmpty(t, priv)
+	assert.NotEmpty(t, pub)
+
+	assert.Regexp(t, regexp.MustCompile("^-----BEGIN RSA PRIVATE KEY-----.*"), priv)
+	assert.Regexp(t, regexp.MustCompile("^-----BEGIN PUBLIC KEY-----.*"), pub)
+
+}
+
+func TestSignUsingKeys(t *testing.T) {
+	priv, pub, err := GenerateKeyPair()
+	assert.NoError(t, err)
+
+	privPem, _ := pem.Decode([]byte(priv))
+	if privPem == nil || privPem.Type != "RSA PRIVATE KEY" {
+		t.Fatal("key is wrong type")
+	}
+
+	privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
+	assert.NoError(t, err)
+
+	pubPem, _ := pem.Decode([]byte(pub))
+	if pubPem == nil || pubPem.Type != "PUBLIC KEY" {
+		t.Fatal("key failed to decode")
+	}
+
+	pubParsed, err := x509.ParsePKIXPublicKey(pubPem.Bytes)
+	assert.NoError(t, err)
+
+	// Sign
+	msg := "activity pub is great!"
+	h := sha256.New()
+	h.Write([]byte(msg))
+	d := h.Sum(nil)
+	sig, err := rsa.SignPKCS1v15(rand.Reader, privParsed, crypto.SHA256, d)
+	assert.NoError(t, err)
+
+	// Verify
+	err = rsa.VerifyPKCS1v15(pubParsed.(*rsa.PublicKey), crypto.SHA256, d, sig)
+	assert.NoError(t, err)
+}