mautrix-go/crypto/signatures/signatures.go

95 lines
2.8 KiB
Go

// Copyright (c) 2024 Sumner Evans
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package signatures
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"go.mau.fi/util/exgjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/goolm/crypto"
"maunium.net/go/mautrix/id"
)
var (
ErrEmptyInput = errors.New("empty input")
ErrSignatureNotFound = errors.New("input JSON doesn't contain signature from specified device")
)
// Signatures represents a set of signatures for some data from multiple users
// and keys.
type Signatures map[id.UserID]map[id.KeyID]string
// NewSingleSignature creates a new [Signatures] object with a single
// signature.
func NewSingleSignature(userID id.UserID, algorithm id.KeyAlgorithm, keyID string, signature string) Signatures {
return Signatures{
userID: {
id.NewKeyID(algorithm, keyID): signature,
},
}
}
// VerifySignature verifies an Ed25519 signature.
func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) {
if len(message) == 0 || len(key) == 0 || len(signature) == 0 {
return false, ErrEmptyInput
}
keyDecoded, err := base64.RawStdEncoding.DecodeString(key.String())
if err != nil {
return false, err
}
publicKey := crypto.Ed25519PublicKey(keyDecoded)
return publicKey.Verify(message, signature), nil
}
// VerifySignatureJSON verifies the signature in the given JSON object "obj"
// as described in [Appendix 3] of the Matrix Spec.
//
// This function is a wrapper over [Utility.VerifySignatureJSON] that creates
// and destroys the [Utility] object transparently.
//
// If the "obj" is not already a [json.RawMessage], it will re-encoded as JSON
// for the verification, so "json" tags will be honored.
//
// [Appendix 3]: https://spec.matrix.org/v1.9/appendices/#signing-json
func VerifySignatureJSON(obj any, userID id.UserID, keyName string, key id.Ed25519) (bool, error) {
var err error
objJSON, ok := obj.(json.RawMessage)
if !ok {
objJSON, err = json.Marshal(obj)
if err != nil {
return false, err
}
}
sig := gjson.GetBytes(objJSON, exgjson.Path("signatures", string(userID), fmt.Sprintf("ed25519:%s", keyName)))
if !sig.Exists() || sig.Type != gjson.String {
return false, ErrSignatureNotFound
}
objJSON, err = sjson.DeleteBytes(objJSON, "unsigned")
if err != nil {
return false, err
}
objJSON, err = sjson.DeleteBytes(objJSON, "signatures")
if err != nil {
return false, err
}
objJSONString := canonicaljson.CanonicalJSONAssumeValid(objJSON)
sigBytes, err := base64.RawStdEncoding.DecodeString(sig.Str)
if err != nil {
return false, err
}
return VerifySignature(objJSONString, key, sigBytes)
}