diff --git a/activitypub/import.go b/activitypub/import.go index d91d665..05d69d6 100644 --- a/activitypub/import.go +++ b/activitypub/import.go @@ -77,6 +77,7 @@ func ImportRemoteAccount(targetName string) (string, error) { // Sign and send err = webshared.SignRequest(req, linstromActor.ID+"#main-key", keyBytes, nil) + // err = webshared.SignWithHttpsig(req, linstromActor.ID+"#main-key", keyBytes, nil) if err != nil { return "", err } diff --git a/go.mod b/go.mod index e31745f..b8bb3c4 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.7.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-webauthn/x v0.1.14 // indirect diff --git a/go.sum b/go.sum index 076401e..87d76b8 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8 github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ= github.com/gen2brain/avif v0.3.2/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/shared/signing.go b/shared/signing.go index 568ddf1..c07a5e9 100644 --- a/shared/signing.go +++ b/shared/signing.go @@ -6,9 +6,16 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + "crypto/sha512" "crypto/x509" + "encoding/pem" + "errors" + + "git.mstar.dev/mstar/linstrom/config" ) +const sanityCheckRawMessage = "test message for sanity checking keys" + func GenerateKeypair(useEd bool) (publicKey []byte, privateKey []byte, err error) { if useEd { publicKey, privateKey, err := ed25519.GenerateKey(nil) @@ -38,12 +45,103 @@ func Sign(toSign string, keyBytes []byte, keyIsRsa bool) ([]byte, error) { if err != nil { return nil, err } - hash := sha256.Sum256([]byte(toSign)) - signed, err := key.Sign(rand.Reader, hash[:], crypto.SHA256) + hasher := sha256.New() + _, err = hasher.Write([]byte(toSign)) + if err != nil { + return nil, err + } + // hash := sha256.Sum256([]byte(toSign)) + hash := hasher.Sum(nil) + signed, err := rsa.SignPKCS1v15(nil, key, crypto.SHA256, hash) + // signed, err := key.Sign(rand.Reader, hash[:], crypto.SHA256) return signed, err } else { key := ed25519.PrivateKey(keyBytes) - signed, err := key.Sign(rand.Reader, []byte(toSign), crypto.SHA256) + hash := sha512.Sum512([]byte(toSign)) + signed, err := key.Sign(rand.Reader, hash[:], crypto.SHA512) return signed, err } } + +func KeyBytesToPem(bytes []byte) string { + var t string + if config.GlobalConfig.Experimental.UseEd25519Keys { + t = "PUBLIC KEY" + } else { + // t = "RSA PUBLIC KEY" + t = "PUBLIC KEY" + } + block := pem.Block{ + Type: t, + Headers: nil, + Bytes: bytes, + } + return string(pem.EncodeToMemory(&block)) +} + +func SanityCheckRawEdKeys(pub ed25519.PublicKey, priv ed25519.PrivateKey) error { + hash := sha512.Sum512([]byte(sanityCheckRawMessage)) + signed, err := priv.Sign(rand.Reader, hash[:], crypto.SHA512) + if err != nil { + return err + } + return ed25519.VerifyWithOptions(pub, hash[:], signed, &ed25519.Options{ + Hash: crypto.SHA512, + }) +} + +func SanityCheckRawByteEdKeys(pub, priv []byte) error { + pubKey := ed25519.PublicKey(pub) + privKey := ed25519.PrivateKey(priv) + return SanityCheckRawEdKeys(pubKey, privKey) +} + +func SanityCheckX509dEdKeys(pub, priv []byte) error { + privKey := ed25519.PrivateKey(priv) + rawPubKey, err := x509.ParsePKIXPublicKey(pub) + if err != nil { + return err + } + pubKey, ok := rawPubKey.(ed25519.PublicKey) + if !ok { + return errors.New("not an ed25519 key") + } + return SanityCheckRawEdKeys(pubKey, privKey) +} + +func SanityCheckPemdEdKeys(pub, priv []byte) error { + privBlock, _ := pem.Decode(priv) + pubBlock, _ := pem.Decode(pub) + return SanityCheckX509dEdKeys(pubBlock.Bytes, privBlock.Bytes) +} + +func SanityCheckRawRsaKeys(pub *rsa.PublicKey, priv *rsa.PrivateKey) error { + hash := sha256.Sum256([]byte(sanityCheckRawMessage)) + signed, err := priv.Sign(rand.Reader, hash[:], crypto.SHA256) + if err != nil { + return err + } + return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], signed) +} + +func SanityCheckX509dRsaKeys(pub, priv []byte) error { + privKey, err := x509.ParsePKCS1PrivateKey(priv) + if err != nil { + return err + } + rawPubKey, err := x509.ParsePKIXPublicKey(pub) + if err != nil { + return err + } + pubKey, ok := rawPubKey.(*rsa.PublicKey) + if !ok { + return errors.New("not an rsa key") + } + return SanityCheckRawRsaKeys(pubKey, privKey) +} + +func SanityCheckPemdRsaKeys(pub, priv []byte) error { + privBlock, _ := pem.Decode(priv) + pubBlock, _ := pem.Decode(pub) + return SanityCheckX509dRsaKeys(pubBlock.Bytes, privBlock.Bytes) +} diff --git a/web/debug/server.go b/web/debug/server.go index b04787d..eb42afc 100644 --- a/web/debug/server.go +++ b/web/debug/server.go @@ -28,6 +28,7 @@ func New(addr string) *Server { handler.HandleFunc("POST /post-as", postAs) handler.HandleFunc("GET /notes-for", notesFrom) handler.HandleFunc("GET /import", issueUserImport) + handler.HandleFunc("GET /keys-for", returnKeypair) web := http.Server{ Addr: addr, Handler: webutils.ChainMiddlewares( diff --git a/web/debug/users.go b/web/debug/users.go index e8982b2..a13fabd 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "database/sql" "encoding/json" + "encoding/pem" + "fmt" "net/http" "strconv" "time" @@ -173,6 +175,33 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func returnKeypair(w http.ResponseWriter, r *http.Request) { + id := r.FormValue("id") + user, err := dbgen.User.Where(dbgen.User.ID.Eq(id)).First() + if err != nil { + return + } + err = shared.SanityCheckX509dRsaKeys(user.PublicKeyRsa, user.PrivateKeyRsa) + if err != nil { + hlog.FromRequest(r).Error().Err(err).Msg("Sanity check failed") + } + privKeyBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: user.PrivateKeyRsa, + } + + if err != nil { + hlog.FromRequest(r).Error().Err(err).Msg("Sanity check failed") + } + privKeyPem := pem.EncodeToMemory(&privKeyBlock) + pubKeyPen := []byte(shared.KeyBytesToPem(user.PublicKeyRsa)) + err = shared.SanityCheckPemdRsaKeys(pubKeyPen, privKeyPem) + if err != nil { + hlog.FromRequest(r).Error().Err(err).Msg("Pem Sanity check failed") + } + fmt.Fprintf(w, "%s\n\n%s", privKeyPem, pubKeyPen) +} + func issueUserImport(w http.ResponseWriter, r *http.Request) { target := r.FormValue("target") _, err := activitypub.ImportRemoteAccount(target) diff --git a/web/public/api/activitypub/user.go b/web/public/api/activitypub/user.go index 5e95059..5345738 100644 --- a/web/public/api/activitypub/user.go +++ b/web/public/api/activitypub/user.go @@ -14,6 +14,7 @@ import ( "git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" @@ -71,9 +72,9 @@ func users(w http.ResponseWriter, r *http.Request) { apUrl := userIdToApUrl(user.ID) var keyBytes string if config.GlobalConfig.Experimental.UseEd25519Keys { - keyBytes = keyBytesToPem(user.PublicKeyEd) + keyBytes = shared.KeyBytesToPem(user.PublicKeyEd) } else { - keyBytes = keyBytesToPem(user.PublicKeyRsa) + keyBytes = shared.KeyBytesToPem(user.PublicKeyRsa) } data := Outbound{ Context: activitypub.BaseLdContext, diff --git a/web/public/api/activitypub/util.go b/web/public/api/activitypub/util.go index 631128a..ca82986 100644 --- a/web/public/api/activitypub/util.go +++ b/web/public/api/activitypub/util.go @@ -1,7 +1,6 @@ package activitypub import ( - "encoding/pem" "fmt" "git.mstar.dev/mstar/linstrom/config" @@ -14,12 +13,3 @@ func userIdToApUrl(id string) string { id, ) } - -func keyBytesToPem(bytes []byte) string { - block := pem.Block{ - Type: "PUBLIC KEY", - Headers: nil, - Bytes: bytes, - } - return string(pem.EncodeToMemory(&block)) -} diff --git a/web/shared/client.go b/web/shared/client.go index a6130c1..2ce109b 100644 --- a/web/shared/client.go +++ b/web/shared/client.go @@ -1,13 +1,17 @@ package webshared import ( + "crypto" + "crypto/ed25519" "crypto/sha256" + "crypto/x509" "io" "net/http" "strings" "time" "git.mstar.dev/mstar/goutils/maputils" + "github.com/go-fed/httpsig" "github.com/rs/zerolog/log" "git.mstar.dev/mstar/linstrom/config" @@ -66,7 +70,6 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte signedString = tmp usedHeaders = tmp2 } - log.Debug().Str("string-to-sign", signedString).Any("headers", mappedHeaders).Send() signature := CreateSignatureHeaderContent( keyId, signedString, @@ -77,6 +80,46 @@ func SignRequest(r *http.Request, keyId string, privateKeyBytes, postBody []byte return nil } +func SignWithHttpsig(r *http.Request, keyId string, privateKeyBytes, postBody []byte) error { + var privateKey crypto.PrivateKey + var preferredAlgorithm []httpsig.Algorithm + var digestMethod httpsig.DigestAlgorithm + if config.GlobalConfig.Experimental.UseEd25519Keys { + log.Debug().Msg("Using ed25519") + preferredAlgorithm = []httpsig.Algorithm{httpsig.ED25519} + privateKey = ed25519.PrivateKey(privateKeyBytes) + digestMethod = httpsig.DigestSha512 + } else { + log.Debug().Msg("Using rsa") + preferredAlgorithm = []httpsig.Algorithm{httpsig.RSA_SHA256} + key, err := x509.ParsePKCS1PrivateKey(privateKeyBytes) + if err != nil { + return err + } + privateKey = key + digestMethod = httpsig.DigestSha256 + } + headers := []string{httpsig.RequestTarget, "date", "host"} + if postBody != nil { + headers = append(headers, "digest") + } + signer, _, err := httpsig.NewSigner( + preferredAlgorithm, + digestMethod, + headers, + httpsig.Signature, time.Now().Add(time.Minute).Unix()) + if err != nil { + return err + } + err = signer.SignRequest( + privateKey, + config.GlobalConfig.General.GetFullPublicUrl()+"/api/activitypub/user/"+keyId, r, postBody) + if err != nil { + return err + } + return nil +} + func applyBodyHash(headers http.Header, body []byte) error { if body == nil { return nil diff --git a/web/shared/signing.go b/web/shared/signing.go index 533890d..d40705a 100644 --- a/web/shared/signing.go +++ b/web/shared/signing.go @@ -46,13 +46,14 @@ func genPreSignatureString(method, target string, headers map[string]string) (st dataBuilder.WriteString("(request-target): ") dataBuilder.WriteString(strings.ToLower(method) + " ") dataBuilder.WriteString(target + "\n") - dataBuilder.WriteString("algorithm: rsa-sha256\n") - usedHeaders := []string{"(request-target)", "algorithm"} + // dataBuilder.WriteString("algorithm: rsa-sha256\n") + // usedHeaders := []string{"(request-target)", "algorithm"} + usedHeaders := []string{"(request-target)"} for k, v := range headers { dataBuilder.WriteString(k + ": " + v + "\n") usedHeaders = append(usedHeaders, k) } - tmp := dataBuilder.String() + tmp := strings.TrimSuffix(dataBuilder.String(), "\n") log.Debug().Str("Raw signature string", tmp).Send() return tmp, usedHeaders } @@ -73,7 +74,11 @@ func CreateSignatureHeaderContent(userId string, hash string, headerNames ...str builder.WriteRune(' ') } } - builder.WriteString("\",algorithm=\"rsa-sha256\",signature=\"") + if config.GlobalConfig.Experimental.UseEd25519Keys { + builder.WriteString("\",algorithm=\"ed-sha512\",signature=\"") + } else { + builder.WriteString("\",algorithm=\"rsa-sha256\",signature=\"") + } builder.WriteString(hash) builder.WriteRune('"')