mirror of
https://github.com/versia-pub/versia-go.git
synced 2026-03-13 20:49:15 +01:00
refactor!: working WD-4 user discovery
This commit is contained in:
parent
cf0053312d
commit
61891d891a
91 changed files with 12768 additions and 5562 deletions
|
|
@ -1,15 +1,7 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// User represents a user object in the Lysand protocol. For more information, see the [Spec].
|
||||
|
|
@ -20,7 +12,7 @@ type User struct {
|
|||
|
||||
// PublicKey is the public key of the user.
|
||||
// https://lysand.org/objects/user#public-key
|
||||
PublicKey PublicKey `json:"public_key"`
|
||||
PublicKey UserPublicKey `json:"public_key"`
|
||||
|
||||
// DisplayName is the display name of the user.
|
||||
// https://lysand.org/objects/user#display-name
|
||||
|
|
@ -66,14 +58,6 @@ type User struct {
|
|||
// https://lysand.org/objects/user#following
|
||||
Following *URL `json:"following"`
|
||||
|
||||
// Likes is the likes of the user.
|
||||
// https://lysand.org/objects/user#likes
|
||||
Likes *URL `json:"likes"`
|
||||
|
||||
// Dislikes is the dislikes of the user.
|
||||
// https://lysand.org/objects/user#dislikes
|
||||
Dislikes *URL `json:"dislikes"`
|
||||
|
||||
// Inbox is the inbox of the user.
|
||||
// https://lysand.org/objects/user#posts
|
||||
Inbox *URL `json:"inbox"`
|
||||
|
|
@ -94,60 +78,3 @@ type Field struct {
|
|||
Key TextContentTypeMap `json:"key"`
|
||||
Value TextContentTypeMap `json:"value"`
|
||||
}
|
||||
|
||||
func (c *FederationClient) GetUser(ctx context.Context, uri *url.URL) (*User, error) {
|
||||
resp, body, err := c.rawGET(ctx, uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &User{}
|
||||
if err := json.Unmarshal(body, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fedHeaders, err := ExtractFederationHeaders(resp.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
||||
if !v.Verify("GET", uri, body, fedHeaders) {
|
||||
c.log.V(2).Info("signature verification failed", "user", user.URI.String())
|
||||
return nil, fmt.Errorf("signature verification failed")
|
||||
}
|
||||
c.log.V(2).Info("signature verification succeeded", "user", user.URI.String())
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (c *FederationClient) SendToInbox(ctx context.Context, signer Signer, user *User, object any) ([]byte, error) {
|
||||
uri := user.Inbox.ToStd()
|
||||
|
||||
body, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, 32)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigData := NewSignatureData("POST", base64.StdEncoding.EncodeToString(nonce), uri, hashSHA256(body))
|
||||
sig := signer.Sign(*sigData)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig.Inject(req.Header)
|
||||
|
||||
_, respBody, err := c.doReq(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,5 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (c *FederationClient) ValidateSignatureHeader(req *http.Request) (bool, error) {
|
||||
fedHeaders, err := ExtractFederationHeaders(req.Header)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// TODO: Fetch user from database instead of using the URI
|
||||
user, err := c.GetUser(req.Context(), fedHeaders.SignedBy)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
body, err := copyBody(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
||||
valid := v.Verify(req.Method, req.URL, body, fedHeaders)
|
||||
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
func hashSHA256(data []byte) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func must[In any, Out any](fn func(In) (Out, error), v In) Out {
|
||||
out, err := fn(v)
|
||||
if err != nil {
|
||||
|
|
@ -45,17 +8,3 @@ func must[In any, Out any](fn func(In) (Out, error), v In) Out {
|
|||
|
||||
return out
|
||||
}
|
||||
|
||||
func copyBody(req *http.Request) ([]byte, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
return body, nil
|
||||
}
|
||||
|
|
|
|||
23
pkg/lysand/crypto/keys.go
Normal file
23
pkg/lysand/crypto/keys.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package versiacrypto
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UnknownPublicKeyTypeError struct {
|
||||
Got string
|
||||
}
|
||||
|
||||
func (i UnknownPublicKeyTypeError) Error() string {
|
||||
return fmt.Sprintf("unknown public key type: \"%s\"", i.Got)
|
||||
}
|
||||
|
||||
func ToTypedKey(algorithm string, raw []byte) (any, error) {
|
||||
switch algorithm {
|
||||
case "ed25519":
|
||||
return ed25519.PublicKey(raw), nil
|
||||
default:
|
||||
return nil, UnknownPublicKeyTypeError{algorithm}
|
||||
}
|
||||
}
|
||||
9
pkg/lysand/crypto/sha256.go
Normal file
9
pkg/lysand/crypto/sha256.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package versiacrypto
|
||||
|
||||
import "crypto/sha256"
|
||||
|
||||
func SHA256(data []byte) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
29
pkg/lysand/crypto/verify.go
Normal file
29
pkg/lysand/crypto/verify.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package versiacrypto
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type InvalidPublicKeyTypeError struct {
|
||||
Got reflect.Type
|
||||
}
|
||||
|
||||
func (i InvalidPublicKeyTypeError) Error() string {
|
||||
return fmt.Sprintf("failed to convert public key of type \"%s\"", i.Got.String())
|
||||
}
|
||||
|
||||
type Verify = func(data, signature []byte) bool
|
||||
|
||||
func NewVerify(pubKey crypto.PublicKey) (Verify, error) {
|
||||
switch pk := pubKey.(type) {
|
||||
case ed25519.PublicKey:
|
||||
return func(data, signature []byte) bool {
|
||||
return ed25519.Verify(pk, data, signature)
|
||||
}, nil
|
||||
default:
|
||||
return nil, InvalidPublicKeyTypeError{reflect.TypeOf(pk)}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-logr/logr"
|
||||
"io"
|
||||
"github.com/lysand-org/versia-go/pkg/protoretry"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
|
@ -22,6 +21,7 @@ func (e *ResponseError) Error() string {
|
|||
type FederationClient struct {
|
||||
log logr.Logger
|
||||
httpC *http.Client
|
||||
hc *protoretry.Client
|
||||
}
|
||||
|
||||
type Opt func(c *FederationClient)
|
||||
|
|
@ -53,49 +53,11 @@ func NewClient(opts ...Opt) *FederationClient {
|
|||
useragent: "github.com/lysand-org/versia-go/pkg/lysand#0.0.1",
|
||||
}
|
||||
|
||||
c.hc = protoretry.New(c.httpC)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *FederationClient) rawGET(ctx context.Context, uri *url.URL) (*http.Response, []byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", uri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.doReq(req)
|
||||
}
|
||||
|
||||
func (c *FederationClient) rawPOST(ctx context.Context, uri *url.URL, body io.Reader) (*http.Response, []byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c.doReq(req)
|
||||
}
|
||||
|
||||
func (c *FederationClient) doReq(req *http.Request) (*http.Response, []byte, error) {
|
||||
resp, err := c.httpC.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return resp, nil, &ResponseError{
|
||||
StatusCode: resp.StatusCode,
|
||||
URL: req.URL,
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp, nil, err
|
||||
}
|
||||
|
||||
return resp, respBody, nil
|
||||
}
|
||||
|
||||
type federationClientHTTPTransport struct {
|
||||
inner http.RoundTripper
|
||||
useragent string
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ func (f *FederationHeaders) Inject(h http.Header) {
|
|||
h.Set("x-signature", base64.StdEncoding.EncodeToString(f.Signature))
|
||||
}
|
||||
|
||||
func (f *FederationHeaders) Headers() map[string]string {
|
||||
return map[string]string{
|
||||
"x-signed-by": f.SignedBy.String(),
|
||||
"x-nonce": f.Nonce,
|
||||
"x-signature": base64.StdEncoding.EncodeToString(f.Signature),
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractFederationHeaders(h http.Header) (*FederationHeaders, error) {
|
||||
signedBy := h.Get("x-signed-by")
|
||||
if signedBy == "" {
|
||||
|
|
@ -41,14 +49,19 @@ func ExtractFederationHeaders(h http.Header) (*FederationHeaders, error) {
|
|||
return nil, fmt.Errorf("missing x-nonce header")
|
||||
}
|
||||
|
||||
signature := h.Get("x-signature")
|
||||
if signature == "" {
|
||||
rawSignature := h.Get("x-signature")
|
||||
if rawSignature == "" {
|
||||
return nil, fmt.Errorf("missing x-signature header")
|
||||
}
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(rawSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FederationHeaders{
|
||||
SignedBy: u,
|
||||
Nonce: nonce,
|
||||
Signature: []byte(signature),
|
||||
Signature: signature,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
111
pkg/lysand/instance_metadata.go
Normal file
111
pkg/lysand/instance_metadata.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// InstanceMetadata represents the metadata of a Lysand instance. For more information, see the [Spec].
|
||||
//
|
||||
// ! Unlike other entities, instance metadata is not meant to be federated.
|
||||
//
|
||||
// [Spec]: https://versia.pub/entities/instance-metadata
|
||||
type InstanceMetadata struct {
|
||||
// Type is always "InstanceMetadata"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Extensions is a map of active extensions
|
||||
Extensions Extensions `json:"extensions,omitempty"`
|
||||
|
||||
// Name is the name of the instance
|
||||
Name string `json:"name"`
|
||||
|
||||
// Description is a description of the instance
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Host is the hostname of the instance, including the port
|
||||
Host string `json:"host,omitempty"`
|
||||
|
||||
// PublicKey is the public key of the instance
|
||||
PublicKey InstancePublicKey `json:"public_key"`
|
||||
|
||||
// SharedInbox is the URL to the instance's shared inbox
|
||||
SharedInbox *URL `json:"shared_inbox,omitempty"`
|
||||
|
||||
// Moderators is a URL to a collection of moderators
|
||||
Moderators *URL `json:"moderators,omitempty"`
|
||||
|
||||
// Admins is a URL to a collection of administrators
|
||||
Admins *URL `json:"admins,omitempty"`
|
||||
|
||||
// Logo is the URL to the instance's logo
|
||||
Logo *ImageContentTypeMap `json:"logo,omitempty"`
|
||||
|
||||
// Banner is the URL to the instance's banner
|
||||
Banner *ImageContentTypeMap `json:"banner,omitempty"`
|
||||
|
||||
// Software is information about the instance software
|
||||
Software InstanceSoftware `json:"software"`
|
||||
|
||||
// Compatibility is information about the instance's compatibility with different Lysand versions
|
||||
Compatibility InstanceCompatibility `json:"compatibility"`
|
||||
}
|
||||
|
||||
func (s InstanceMetadata) MarshalJSON() ([]byte, error) {
|
||||
type instanceMetadata InstanceMetadata
|
||||
s2 := instanceMetadata(s)
|
||||
s2.Type = "InstanceMetadata"
|
||||
return json.Marshal(s2)
|
||||
}
|
||||
|
||||
// InstanceSoftware represents the software of a Lysand instance.
|
||||
type InstanceSoftware struct {
|
||||
// Name is the name of the instance software
|
||||
Name string `json:"name"`
|
||||
// Version is the version of the instance software
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// InstanceCompatibility represents the compatibility of a Lysand instance.
|
||||
type InstanceCompatibility struct {
|
||||
// Versions is a list of versions of Lysand the instance is compatible with
|
||||
Versions []string `json:"versions"`
|
||||
|
||||
// Extensions is a list of extensions supported by the instance
|
||||
Extensions []string `json:"extensions"`
|
||||
}
|
||||
|
||||
// InstancePublicKey represents the public key of a Versia instance.
|
||||
type InstancePublicKey struct {
|
||||
// Algorithm can only be `ed25519` for now
|
||||
Algorithm string `json:"algorithm"`
|
||||
|
||||
Key *SPKIPublicKey `json:"-"`
|
||||
RawKey json.RawMessage `json:"key"`
|
||||
}
|
||||
|
||||
func (k *InstancePublicKey) UnmarshalJSON(raw []byte) error {
|
||||
type t InstancePublicKey
|
||||
k2 := (*t)(k)
|
||||
if err := json.Unmarshal(raw, k2); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if k2.Key, err = unmarshalSPKIPubKey(k2.Algorithm, k2.RawKey); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k InstancePublicKey) MarshalJSON() ([]byte, error) {
|
||||
type t InstancePublicKey
|
||||
k2 := t(k)
|
||||
|
||||
var err error
|
||||
if k2.RawKey, err = k2.Key.MarshalJSON(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(k2)
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
)
|
||||
|
||||
// ServerMetadata represents the metadata of a Lysand server. For more information, see the [Spec].
|
||||
//
|
||||
// ! Unlike other objects, server metadata is not meant to be federated.
|
||||
//
|
||||
// [Spec]: https://lysand.org/objects/server-metadata
|
||||
type ServerMetadata struct {
|
||||
// Type is always "ServerMetadata"
|
||||
// https://lysand.org/objects/server-metadata#type
|
||||
Type string `json:"type"`
|
||||
|
||||
// Extensions is a map of active extensions
|
||||
// https://lysand.org/objects/server-metadata#extensions
|
||||
Extensions Extensions `json:"extensions,omitempty"`
|
||||
|
||||
// Name is the name of the server
|
||||
// https://lysand.org/objects/server-metadata#name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Version is the version of the server software
|
||||
// https://lysand.org/objects/server-metadata#version
|
||||
Version *semver.Version `json:"version"`
|
||||
|
||||
// Description is a description of the server
|
||||
// https://lysand.org/objects/server-metadata#description
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Website is the URL to the server's website
|
||||
// https://lysand.org/objects/server-metadata#website
|
||||
Website *URL `json:"website,omitempty"`
|
||||
|
||||
// Moderators is a list of URLs to moderators
|
||||
// https://lysand.org/objects/server-metadata#moderators
|
||||
Moderators []*URL `json:"moderators,omitempty"`
|
||||
|
||||
// Admins is a list of URLs to administrators
|
||||
// https://lysand.org/objects/server-metadata#admins
|
||||
Admins []*URL `json:"admins,omitempty"`
|
||||
|
||||
// Logo is the URL to the server's logo
|
||||
// https://lysand.org/objects/server-metadata#logo
|
||||
Logo *ImageContentTypeMap `json:"logo,omitempty"`
|
||||
|
||||
// Banner is the URL to the server's banner
|
||||
// https://lysand.org/objects/server-metadata#banner
|
||||
Banner *ImageContentTypeMap `json:"banner,omitempty"`
|
||||
|
||||
// SupportedExtensions is a list of supported extensions
|
||||
SupportedExtensions []string `json:"supported_extensions"`
|
||||
}
|
||||
|
||||
func (s ServerMetadata) MarshalJSON() ([]byte, error) {
|
||||
type serverMetadata ServerMetadata
|
||||
s2 := serverMetadata(s)
|
||||
s2.Type = "ServerMetadata"
|
||||
return json.Marshal(s2)
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
versiacrypto "github.com/lysand-org/versia-go/pkg/lysand/crypto"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -32,8 +35,16 @@ func (s *SignatureData) String() string {
|
|||
return fmt.Sprintf("%s %s?%s %s %s", strings.ToLower(s.RequestMethod), s.URL.Path, s.URL.RawQuery, s.Nonce, base64.StdEncoding.EncodeToString(s.Digest))
|
||||
}
|
||||
|
||||
func (s *SignatureData) Validate(pubKey ed25519.PublicKey, signature []byte) bool {
|
||||
return ed25519.Verify(pubKey, []byte(s.String()), signature)
|
||||
func (s *SignatureData) Validate(pubKey crypto.PublicKey, signature []byte) bool {
|
||||
data := []byte(s.String())
|
||||
|
||||
verify, err := versiacrypto.NewVerify(pubKey)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return verify(data, signature)
|
||||
}
|
||||
|
||||
func (s *SignatureData) Sign(privKey ed25519.PrivateKey) []byte {
|
||||
|
|
@ -54,11 +65,10 @@ func (s Signer) Sign(signatureData SignatureData) *FederationHeaders {
|
|||
}
|
||||
|
||||
type Verifier struct {
|
||||
PublicKey ed25519.PublicKey
|
||||
PublicKey crypto.PublicKey
|
||||
}
|
||||
|
||||
func (v Verifier) Verify(method string, u *url.URL, body []byte, fedHeaders *FederationHeaders) bool {
|
||||
sigData := NewSignatureData(method, fedHeaders.Nonce, u, hashSHA256(body))
|
||||
|
||||
return sigData.Validate(v.PublicKey, fedHeaders.Signature)
|
||||
return NewSignatureData(method, fedHeaders.Nonce, u, versiacrypto.SHA256(body)).
|
||||
Validate(v.PublicKey, fedHeaders.Signature)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,92 @@
|
|||
package lysand
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidPublicKeyType = errors.New("invalid public key type")
|
||||
)
|
||||
|
||||
// PublicKey represents a public key for a user. For more information, see the [Spec].
|
||||
// UserPublicKey represents a public key for a user. For more information, see the [Spec].
|
||||
//
|
||||
// [Spec]: https://lysand.org/security/keys#public-key-cryptography
|
||||
type PublicKey struct {
|
||||
PublicKey SPKIPublicKey `json:"public_key"`
|
||||
Actor *URL `json:"actor"`
|
||||
type UserPublicKey struct {
|
||||
Actor *URL `json:"actor"`
|
||||
|
||||
// Algorithm can only be `ed25519` for now
|
||||
Algorithm string `json:"algorithm"`
|
||||
|
||||
Key *SPKIPublicKey `json:"-"`
|
||||
RawKey json.RawMessage `json:"key"`
|
||||
}
|
||||
|
||||
// SPKIPublicKey is a type that represents a [ed25519.PublicKey] in the SPKI
|
||||
// format.
|
||||
type SPKIPublicKey ed25519.PublicKey
|
||||
func (k *UserPublicKey) UnmarshalJSON(raw []byte) error {
|
||||
type t UserPublicKey
|
||||
k2 := (*t)(k)
|
||||
|
||||
// UnmarshalJSON decodes the public key from a base64 encoded string and then unmarshals it from the SPKI form.
|
||||
func (k *SPKIPublicKey) UnmarshalJSON(raw []byte) error {
|
||||
rawStr := ""
|
||||
if err := json.Unmarshal(raw, &rawStr); err != nil {
|
||||
if err := json.Unmarshal(raw, k2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw, err := base64.StdEncoding.DecodeString(rawStr)
|
||||
if err != nil {
|
||||
var err error
|
||||
if k2.Key, err = unmarshalSPKIPubKey(k2.Algorithm, k2.RawKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsed, err := x509.ParsePKIXPublicKey(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edKey, ok := parsed.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return ErrInvalidPublicKeyType
|
||||
}
|
||||
|
||||
*k = SPKIPublicKey(edKey)
|
||||
*k = UserPublicKey(*k2)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k UserPublicKey) MarshalJSON() ([]byte, error) {
|
||||
type t UserPublicKey
|
||||
k2 := t(k)
|
||||
|
||||
var err error
|
||||
if k2.RawKey, err = k2.Key.MarshalJSON(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(k2)
|
||||
}
|
||||
|
||||
// SPKIPublicKey is a type that represents a [ed25519.PublicKey] in the SPKI
|
||||
// format.
|
||||
type SPKIPublicKey struct {
|
||||
Key any
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func unmarshalSPKIPubKey(algorithm string, raw []byte) (*SPKIPublicKey, error) {
|
||||
rawStr := ""
|
||||
if err := json.Unmarshal(raw, &rawStr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := base64.StdEncoding.DecodeString(rawStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewSPKIPubKey(algorithm, raw)
|
||||
}
|
||||
|
||||
// NewSPKIPubKey decodes the public key from a base64 encoded string and then unmarshals it from the SPKI form.
|
||||
func NewSPKIPubKey(algorithm string, raw []byte) (*SPKIPublicKey, error) {
|
||||
parsed, err := x509.ParsePKIXPublicKey(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SPKIPublicKey{
|
||||
Key: parsed,
|
||||
Algorithm: algorithm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the SPKI-encoded public key to a base64 encoded string.
|
||||
func (k SPKIPublicKey) MarshalJSON() ([]byte, error) {
|
||||
raw, err := x509.MarshalPKIXPublicKey(ed25519.PublicKey(k))
|
||||
raw, err := x509.MarshalPKIXPublicKey(k.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -61,6 +94,6 @@ func (k SPKIPublicKey) MarshalJSON() ([]byte, error) {
|
|||
return json.Marshal(base64.StdEncoding.EncodeToString(raw))
|
||||
}
|
||||
|
||||
func (k SPKIPublicKey) ToStd() ed25519.PublicKey {
|
||||
return ed25519.PublicKey(k)
|
||||
func (k SPKIPublicKey) ToKey() crypto.PublicKey {
|
||||
return k.Key
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ import (
|
|||
func TestSPKIPublicKey_UnmarshalJSON(t *testing.T) {
|
||||
expectedPk := must(x509.ParsePKIXPublicKey, must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs="))
|
||||
|
||||
pk := PublicKey{}
|
||||
pk := UserPublicKey{}
|
||||
raw := []byte(`{"public_key":"MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs="}`)
|
||||
if err := json.Unmarshal(raw, &pk); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedPk, ed25519.PublicKey(pk.PublicKey))
|
||||
assert.Equal(t, expectedPk, ed25519.PublicKey(pk.Key))
|
||||
}
|
||||
|
||||
func TestSPKIPublicKey_MarshalJSON(t *testing.T) {
|
||||
expectedPk := must(x509.ParsePKIXPublicKey, must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs=")).(ed25519.PublicKey)
|
||||
|
||||
pk := PublicKey{
|
||||
PublicKey: SPKIPublicKey(expectedPk),
|
||||
pk := UserPublicKey{
|
||||
Key: SPKIPublicKey(expectedPk),
|
||||
}
|
||||
if _, err := json.Marshal(pk); err != nil {
|
||||
t.Error(err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue