diff --git a/internal/ap/extract.go b/internal/ap/extract.go index d9288c162..e0c90c5d7 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -515,9 +515,9 @@ func ExtractURL(i WithURL) (*url.URL, error) { return nil, gtserror.New("no valid URL property found") } -// ExtractPublicKey extracts the public key, public key ID, and public +// ExtractPubKeyFromActor extracts the public key, public key ID, and public // key owner ID from an interface, or an error if something goes wrong. -func ExtractPublicKey(i WithPublicKey) ( +func ExtractPubKeyFromActor(i WithPublicKey) ( *rsa.PublicKey, // pubkey *url.URL, // pubkey ID *url.URL, // pubkey owner @@ -528,6 +528,7 @@ func ExtractPublicKey(i WithPublicKey) ( return nil, nil, nil, gtserror.New("public key property was nil") } + // Take the first public key we can find. for iter := pubKeyProp.Begin(); iter != pubKeyProp.End(); iter = iter.Next() { if !iter.IsW3IDSecurityV1PublicKey() { continue @@ -538,63 +539,74 @@ func ExtractPublicKey(i WithPublicKey) ( continue } - pubKeyID, err := pub.GetId(pkey) - if err != nil { - continue - } - - pubKeyOwnerProp := pkey.GetW3IDSecurityV1Owner() - if pubKeyOwnerProp == nil { - continue - } - - pubKeyOwner := pubKeyOwnerProp.GetIRI() - if pubKeyOwner == nil { - continue - } - - pubKeyPemProp := pkey.GetW3IDSecurityV1PublicKeyPem() - if pubKeyPemProp == nil { - continue - } - - pkeyPem := pubKeyPemProp.Get() - if pkeyPem == "" { - continue - } - - block, _ := pem.Decode([]byte(pkeyPem)) - if block == nil { - continue - } - - var p crypto.PublicKey - switch block.Type { - case "PUBLIC KEY": - p, err = x509.ParsePKIXPublicKey(block.Bytes) - case "RSA PUBLIC KEY": - p, err = x509.ParsePKCS1PublicKey(block.Bytes) - default: - err = fmt.Errorf("unknown block type: %q", block.Type) - } - if err != nil { - err = gtserror.Newf("could not parse public key from block bytes: %w", err) - return nil, nil, nil, err - } - - if p == nil { - return nil, nil, nil, gtserror.New("returned public key was empty") - } - - pubKey, ok := p.(*rsa.PublicKey) - if !ok { - continue - } - - return pubKey, pubKeyID, pubKeyOwner, nil + return ExtractPubKeyFromKey(pkey) } - return nil, nil, nil, gtserror.New("couldn't find public key") + return nil, nil, nil, gtserror.New("couldn't find valid public key") +} + +// ExtractPubKeyFromActor extracts the public key, public key ID, and public +// key owner ID from an interface, or an error if something goes wrong. +func ExtractPubKeyFromKey(pkey vocab.W3IDSecurityV1PublicKey) ( + *rsa.PublicKey, // pubkey + *url.URL, // pubkey ID + *url.URL, // pubkey owner + error, +) { + pubKeyID, err := pub.GetId(pkey) + if err != nil { + return nil, nil, nil, errors.New("no id set on public key") + } + + pubKeyOwnerProp := pkey.GetW3IDSecurityV1Owner() + if pubKeyOwnerProp == nil { + return nil, nil, nil, errors.New("nil pubKeyOwnerProp") + } + + pubKeyOwner := pubKeyOwnerProp.GetIRI() + if pubKeyOwner == nil { + return nil, nil, nil, errors.New("nil iri on pubKeyOwnerProp") + } + + pubKeyPemProp := pkey.GetW3IDSecurityV1PublicKeyPem() + if pubKeyPemProp == nil { + return nil, nil, nil, errors.New("nil pubKeyPemProp") + } + + pkeyPem := pubKeyPemProp.Get() + if pkeyPem == "" { + return nil, nil, nil, errors.New("empty pubKeyPemProp") + } + + block, _ := pem.Decode([]byte(pkeyPem)) + if block == nil { + return nil, nil, nil, errors.New("nil pubKeyPem") + } + + var p crypto.PublicKey + switch block.Type { + case "PUBLIC KEY": + p, err = x509.ParsePKIXPublicKey(block.Bytes) + case "RSA PUBLIC KEY": + p, err = x509.ParsePKCS1PublicKey(block.Bytes) + default: + err = fmt.Errorf("unknown block type: %q", block.Type) + } + if err != nil { + err = fmt.Errorf("could not parse public key from block bytes: %w", err) + return nil, nil, nil, err + } + + if p == nil { + return nil, nil, nil, fmt.Errorf("returned public key was empty") + } + + pubKey, ok := p.(*rsa.PublicKey) + if !ok { + return nil, nil, nil, fmt.Errorf("could not type pubKey to *rsa.PublicKey") + } + + return pubKey, pubKeyID, pubKeyOwner, nil } // ExtractContent returns an intermediary representation of diff --git a/internal/ap/extractpubkey_test.go b/internal/ap/extractpubkey_test.go new file mode 100644 index 000000000..f7218f8b1 --- /dev/null +++ b/internal/ap/extractpubkey_test.go @@ -0,0 +1,108 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ap_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/activity/streams" + typepublickey "github.com/superseriousbusiness/activity/streams/impl/w3idsecurityv1/type_publickey" + "github.com/superseriousbusiness/gotosocial/internal/ap" +) + +const ( + stubActor = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1" + ], + "id": "https://gts.superseriousbusiness.org/users/dumpsterqueer", + "preferredUsername": "dumpsterqueer", + "publicKey": { + "id": "https://gts.superseriousbusiness.org/users/dumpsterqueer/main-key", + "owner": "https://gts.superseriousbusiness.org/users/dumpsterqueer", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt7cDz2XfTJXbmmmVXZ3o\nQGB1zu1yP+2/QZZFbLCeM0bMm5cfjJ/olli6kpdcGLh1lFpSgyLE0PlAVNYdSke9\nzcxDao6N16wavFx/bOYhh8HJPPXzlFpNeQQ+EBQ1ivzuLQyzIFTMV4TyZzOREoG9\nizuXuuKDaH/ENDE6qlIDuqtICIjnURjpxnBLldPUxfUvuSO3zY+jTidsxhjUjqkK\nC7RtEVi/D6/CzktVevz5bE/gcAYgKmK0dmkJ9HH6LzOlvkM4Wrq5h/hrM+H1z5e5\nPpdJsl3KlRT4wusuM1Z5xqLQ0oIP4mX/Kd3ypCe150i+jaoCsqBk8OPtl/zKMw1a\nYQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "type": "Person" +}` + + key = `{ + "@context": "https://w3id.org/security/v1", + "id": "https://gts.superseriousbusiness.org/users/dumpsterqueer/main-key", + "owner": "https://gts.superseriousbusiness.org/users/dumpsterqueer", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt7cDz2XfTJXbmmmVXZ3o\nQGB1zu1yP+2/QZZFbLCeM0bMm5cfjJ/olli6kpdcGLh1lFpSgyLE0PlAVNYdSke9\nzcxDao6N16wavFx/bOYhh8HJPPXzlFpNeQQ+EBQ1ivzuLQyzIFTMV4TyZzOREoG9\nizuXuuKDaH/ENDE6qlIDuqtICIjnURjpxnBLldPUxfUvuSO3zY+jTidsxhjUjqkK\nC7RtEVi/D6/CzktVevz5bE/gcAYgKmK0dmkJ9HH6LzOlvkM4Wrq5h/hrM+H1z5e5\nPpdJsl3KlRT4wusuM1Z5xqLQ0oIP4mX/Kd3ypCe150i+jaoCsqBk8OPtl/zKMw1a\nYQIDAQAB\n-----END PUBLIC KEY-----\n" +}` +) + +type ExtractPubKeyTestSuite struct { + APTestSuite +} + +func (suite *ExtractPubKeyTestSuite) TestExtractPubKeyFromStub() { + m := make(map[string]interface{}) + if err := json.Unmarshal([]byte(stubActor), &m); err != nil { + suite.FailNow(err.Error()) + } + + t, err := streams.ToType(context.Background(), m) + if err != nil { + suite.FailNow(err.Error()) + } + + wpk, ok := t.(ap.WithPublicKey) + if !ok { + suite.FailNow("", "could not parse %T as WithPublicKey", t) + } + + pubKey, pubKeyID, ownerURI, err := ap.ExtractPubKeyFromActor(wpk) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.NotNil(pubKey) + suite.Equal("https://gts.superseriousbusiness.org/users/dumpsterqueer/main-key", pubKeyID.String()) + suite.Equal("https://gts.superseriousbusiness.org/users/dumpsterqueer", ownerURI.String()) +} + +func (suite *ExtractPubKeyTestSuite) TestExtractPubKeyFromKey() { + m := make(map[string]interface{}) + if err := json.Unmarshal([]byte(key), &m); err != nil { + suite.FailNow(err.Error()) + } + + pk, err := typepublickey.DeserializePublicKey(m, nil) + if err != nil { + suite.FailNow(err.Error()) + } + + pubKey, pubKeyID, ownerURI, err := ap.ExtractPubKeyFromKey(pk) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.NotNil(pubKey) + suite.Equal("https://gts.superseriousbusiness.org/users/dumpsterqueer/main-key", pubKeyID.String()) + suite.Equal("https://gts.superseriousbusiness.org/users/dumpsterqueer", ownerURI.String()) +} + +func TestExtractPubKeyTestSuite(t *testing.T) { + suite.Run(t, &ExtractPubKeyTestSuite{}) +} diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go index 596233b19..e9263d43c 100644 --- a/internal/federation/authenticate.go +++ b/internal/federation/authenticate.go @@ -30,6 +30,7 @@ import ( "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/streams" + typepublickey "github.com/superseriousbusiness/activity/streams/impl/w3idsecurityv1/type_publickey" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -504,24 +505,45 @@ func parsePubKeyBytes( return nil, nil, err } - t, err := streams.ToType(ctx, m) - if err != nil { - return nil, nil, err + var ( + pubKey *rsa.PublicKey + ownerURI *url.URL + ) + + if t, err := streams.ToType(ctx, m); err == nil { + // See if Actor with a PublicKey attached. + wpk, ok := t.(ap.WithPublicKey) + if !ok { + return nil, nil, gtserror.Newf( + "resource at %s with type %T did not contain recognizable public key", + pubKeyID, t, + ) + } + + pubKey, _, ownerURI, err = ap.ExtractPubKeyFromActor(wpk) + if err != nil { + return nil, nil, gtserror.Newf( + "error extracting public key from %T at %s: %w", + t, pubKeyID, err, + ) + } + } else if pk, err := typepublickey.DeserializePublicKey(m, nil); err == nil { + // Bare PublicKey. + pubKey, _, ownerURI, err = ap.ExtractPubKeyFromKey(pk) + if err != nil { + return nil, nil, gtserror.Newf( + "error extracting public key at %s: %w", + pubKeyID, err, + ) + } + } else { + return nil, nil, gtserror.Newf( + "resource at %s did not contain recognizable public key", + pubKeyID, + ) } - withPublicKey, ok := t.(ap.WithPublicKey) - if !ok { - err = gtserror.Newf("resource at %s with type %T could not be converted to ap.WithPublicKey", pubKeyID, t) - return nil, nil, err - } - - pubKey, _, pubKeyOwnerID, err := ap.ExtractPublicKey(withPublicKey) - if err != nil { - err = gtserror.Newf("resource at %s with type %T did not contain recognizable public key", pubKeyID, t) - return nil, nil, err - } - - return pubKey, pubKeyOwnerID, nil + return pubKey, ownerURI, nil } var signingAlgorithms = []httpsig.Algorithm{ diff --git a/internal/timeline/get_test.go b/internal/timeline/get_test.go index 02256054e..499a9f35d 100644 --- a/internal/timeline/get_test.go +++ b/internal/timeline/get_test.go @@ -688,7 +688,7 @@ func (suite *GetTestSuite) TestGetTimelinesAsync() { limit, local, ); err != nil { - suite.FailNow(err.Error()) + suite.Fail(err.Error()) } wg.Done() diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index b5e713554..ba370790a 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -211,7 +211,7 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a } // Extract account public key and verify ownership to account. - pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPublicKey(accountable) + pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPubKeyFromActor(accountable) if err != nil { err := gtserror.Newf("error extracting public key for %s: %w", uri, err) return nil, gtserror.SetMalformed(err)