Compare commits
9 Commits
a02a6fd302
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7025f0abbb | |||
| 7161f649fa | |||
| b3bae0bf86 | |||
| 67c05fd4a2 | |||
| 526b73aa99 | |||
| 2c3e0b3df6 | |||
| 249b99f46f | |||
| 10fa7919b0 | |||
| 06fca3e3fe |
196
client.go
196
client.go
@@ -62,103 +62,190 @@ type Client struct {
|
|||||||
GetPubkey func(serverName string) (ed25519.PublicKey, error)
|
GetPubkey func(serverName string) (ed25519.PublicKey, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup looks up the profile descriptor for the given ID.
|
// Lookup looks up the profile descriptor for the given resource.
|
||||||
func (c Client) Lookup(id string) (*Descriptor, error) {
|
func (c Client) Lookup(resource string) (*Descriptor, error) {
|
||||||
wfdesc, err := webfinger.LookupAcct(id)
|
wfdesc, err := webfinger.LookupAcct(resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out := &Descriptor{}
|
||||||
|
return out, c.lookup(wfdesc, "", false, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupID looks up the profile descriptor that matches the given ID
|
||||||
|
// for the given resource.
|
||||||
|
func (c Client) LookupID(resource, id string) (*Descriptor, error) {
|
||||||
|
wfdesc, err := webfinger.LookupAcct(resource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Descriptor{}
|
||||||
|
return out, c.lookup(wfdesc, id, false, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup looks up all the available profile descriptors for the given resource.
|
||||||
|
func (c Client) LookupAll(resource string) (map[string]*Descriptor, error) {
|
||||||
|
wfdesc, err := webfinger.LookupAcct(resource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := map[string]*Descriptor{}
|
||||||
|
return out, c.lookup(wfdesc, "", true, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupWebFinger is the same as [Client.Lookup], but it accepts an existing WebFinger
|
||||||
|
// descriptor rather than looking one up.
|
||||||
|
func (c Client) LookupWebFinger(wfdesc *webfinger.Descriptor) (*Descriptor, error) {
|
||||||
|
out := &Descriptor{}
|
||||||
|
return out, c.lookup(wfdesc, "", false, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupWebFingerID is the same as [Client.LookupID], but it accepts an existing WebFinger
|
||||||
|
// descriptor rather than looking one up.
|
||||||
|
func (c Client) LookupWebFingerID(wfdesc *webfinger.Descriptor, id string) (*Descriptor, error) {
|
||||||
|
out := &Descriptor{}
|
||||||
|
return out, c.lookup(wfdesc, id, false, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupAllWebFinger is the same as [Client.LookupAll], but it accepts an existing WebFinger
|
||||||
|
// descriptor rather than looking one up.
|
||||||
|
func (c Client) LookupAllWebFinger(wfdesc *webfinger.Descriptor) (map[string]*Descriptor, error) {
|
||||||
|
out := map[string]*Descriptor{}
|
||||||
|
return out, c.lookup(wfdesc, "", true, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) lookup(wfdesc *webfinger.Descriptor, id string, all bool, dest any) error {
|
||||||
pfdLink, ok := wfdesc.LinkByType("application/x-pfd+json")
|
pfdLink, ok := wfdesc.LinkByType("application/x-pfd+json")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("server does not support the profilefed protocol")
|
return errors.New("server does not support the profilefed protocol")
|
||||||
}
|
}
|
||||||
|
|
||||||
pfdURL, err := url.Parse(pfdLink.Href)
|
pfdURL, err := url.Parse(pfdLink.Href)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pubkeySaved := false
|
pubkeySaved := false
|
||||||
pubkey, err := c.GetPubkey(pfdURL.Host)
|
pubkey, err := c.GetPubkey(pfdURL.Host)
|
||||||
if errors.Is(err, ErrPubkeyNotFound) {
|
if errors.Is(err, ErrPubkeyNotFound) {
|
||||||
info, _, err := getServerInfo(pfdURL.Scheme, pfdURL.Host)
|
data, sig, prevSigs, err := getServerInfo(pfdURL.Scheme, pfdURL.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pubkey, err = base64.StdEncoding.DecodeString(info.Pubkey)
|
var info serverInfoData
|
||||||
|
err = json.Unmarshal(data, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this server is advertising previous names, make sure
|
||||||
|
// we verify that it's telling the truth by checking the whether
|
||||||
|
// any of its signatures match using the pubkeys of the previous names.
|
||||||
|
if len(info.PreviousNames) > 0 {
|
||||||
|
for _, prevName := range info.PreviousNames {
|
||||||
|
pubkey, err = c.GetPubkey(prevName)
|
||||||
|
if errors.Is(err, ErrPubkeyNotFound) {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ed25519.Verify(pubkey, data, sig) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prevSig := range prevSigs {
|
||||||
|
if ed25519.Verify(pubkey, data, prevSig) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we haven't broken out of the loop by now, this
|
||||||
|
// name could not be verified, so return an error.
|
||||||
|
return ErrSignatureMismatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err = base64.StdEncoding.DecodeString(info.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.SavePubkey(pfdURL.Host, info.PreviousNames, pubkey)
|
err = c.SavePubkey(pfdURL.Host, info.PreviousNames, pubkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
pubkeySaved = true
|
pubkeySaved = true
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := http.Get(pfdLink.Href)
|
q := pfdURL.Query()
|
||||||
|
if all {
|
||||||
|
q.Set("all", "1")
|
||||||
|
} else if id != "" {
|
||||||
|
q.Set("id", id)
|
||||||
|
}
|
||||||
|
pfdURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
res, err := http.Get(pfdURL.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if err := checkResp(res, "getProfileDescriptor"); err != nil {
|
if err := checkResp(res, "getProfileDescriptor"); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(io.LimitReader(res.Body, responseSizeLimit))
|
data, err := io.ReadAll(io.LimitReader(res.Body, responseSizeLimit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := res.Body.Close(); err != nil {
|
if err := res.Body.Close(); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sig, err := getSignature(res)
|
sig, err := getSignature(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ed25519.Verify(pubkey, data, sig) {
|
if !ed25519.Verify(pubkey, data, sig) {
|
||||||
// If the pubkey was just saved in the current request, we probably
|
// If the pubkey was just saved in the current request, we probably
|
||||||
// already have the newest one, so just return a mismatch error.
|
// already have the newest one, so just return a mismatch error.
|
||||||
if pubkeySaved {
|
if pubkeySaved {
|
||||||
return nil, ErrSignatureMismatch
|
return ErrSignatureMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := serverInfoReq(pfdURL.Scheme, pfdURL.Host)
|
serverData, infoSig, sigs, err := getServerInfo(pfdURL.Scheme, pfdURL.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
serverData, err := io.ReadAll(io.LimitReader(res.Body, responseSizeLimit))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var info serverInfoData
|
var info serverInfoData
|
||||||
err = json.Unmarshal(serverData, &info)
|
err = json.Unmarshal(serverData, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newPubkey, err := base64.StdEncoding.DecodeString(info.Pubkey)
|
newPubkey, err := base64.StdEncoding.DecodeString(info.PublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the pubkey hasn't changed but we couldn't
|
||||||
|
// verify the signature, return an error immediately.
|
||||||
if bytes.Equal(pubkey, newPubkey) {
|
if bytes.Equal(pubkey, newPubkey) {
|
||||||
return nil, ErrSignatureMismatch
|
return ErrSignatureMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
verified := false
|
verified := false
|
||||||
sigs := getPrevSignatures(res)
|
|
||||||
for _, sig := range sigs {
|
for _, sig := range sigs {
|
||||||
if ed25519.Verify(pubkey, serverData, sig) {
|
if ed25519.Verify(pubkey, serverData, sig) {
|
||||||
verified = true
|
verified = true
|
||||||
@@ -167,66 +254,51 @@ func (c Client) Lookup(id string) (*Descriptor, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !verified {
|
if !verified {
|
||||||
return nil, ErrSignatureMismatch
|
return ErrSignatureMismatch
|
||||||
}
|
|
||||||
|
|
||||||
infoSig, err := getSignature(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ed25519.Verify(newPubkey, infoSig, serverData) {
|
if !ed25519.Verify(newPubkey, infoSig, serverData) {
|
||||||
return nil, ErrSignatureMismatch
|
return ErrSignatureMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.SavePubkey(pfdURL.Host, info.PreviousNames, newPubkey)
|
err = c.SavePubkey(pfdURL.Host, info.PreviousNames, newPubkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ed25519.Verify(newPubkey, data, sig) {
|
if !ed25519.Verify(newPubkey, data, sig) {
|
||||||
return nil, ErrSignatureMismatch
|
return ErrSignatureMismatch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
desc := &Descriptor{}
|
return json.Unmarshal(data, dest)
|
||||||
err = json.Unmarshal(data, desc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if desc.Role == "" {
|
// getServerInfo retrieves server information.
|
||||||
desc.Role = RoleUser
|
func getServerInfo(scheme, host string) (data, sig []byte, prevSigs [][]byte, err error) {
|
||||||
}
|
|
||||||
|
|
||||||
return desc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverInfoReq performs an HTTP request to retrieve server information.
|
|
||||||
func serverInfoReq(scheme, host string) (*http.Response, error) {
|
|
||||||
serverInfoURL := url.URL{
|
serverInfoURL := url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: "/_profilefed/server",
|
Path: "/_profilefed/server",
|
||||||
}
|
}
|
||||||
return http.Get(serverInfoURL.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// getServerInfo retrieves server information.
|
res, err := http.Get(serverInfoURL.String())
|
||||||
func getServerInfo(scheme, host string) (serverInfoData, [][]byte, error) {
|
|
||||||
res, err := serverInfoReq(scheme, host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return serverInfoData{}, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if err := checkResp(res, "getServerInfo"); err != nil {
|
if err := checkResp(res, "getServerInfo"); err != nil {
|
||||||
return serverInfoData{}, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var out serverInfoData
|
sig, err = getSignature(res)
|
||||||
err = json.NewDecoder(io.LimitReader(res.Body, responseSizeLimit)).Decode(&out)
|
if err != nil {
|
||||||
return out, getPrevSignatures(res), err
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = io.ReadAll(io.LimitReader(res.Body, responseSizeLimit))
|
||||||
|
return data, sig, getPrevSignatures(res), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPrevSignatures extracts previous signatures from a response.
|
// getPrevSignatures extracts previous signatures from a response.
|
||||||
|
|||||||
41
keys.go
41
keys.go
@@ -102,3 +102,44 @@ func generateKeys(path string) (ed25519.PublicKey, ed25519.PrivateKey, error) {
|
|||||||
|
|
||||||
return pub, priv, nil
|
return pub, priv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadPrivateKeys loads the private keys at all the provided paths.
|
||||||
|
//
|
||||||
|
// Any invalid keys are skipped.
|
||||||
|
func LoadPrivateKeys(paths ...string) []ed25519.PrivateKey {
|
||||||
|
out := make([]ed25519.PrivateKey, len(paths))
|
||||||
|
for i, path := range paths {
|
||||||
|
privkey, err := LoadPrivateKey(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[i] = privkey
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPrivateKey loads a private Ed25519 key from the given path.
|
||||||
|
func LoadPrivateKey(path string) (ed25519.PrivateKey, error) {
|
||||||
|
privData, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privBlock, _ := pem.Decode(privData)
|
||||||
|
|
||||||
|
if privBlock == nil {
|
||||||
|
return nil, errors.New("invalid private key data")
|
||||||
|
}
|
||||||
|
|
||||||
|
privkey, err := x509.ParsePKCS8PrivateKey(privBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, ok := privkey.(ed25519.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid private key type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ type ServerInfoHandler struct {
|
|||||||
// PreviousNames should contain any previous names this server used.
|
// PreviousNames should contain any previous names this server used.
|
||||||
PreviousNames []string
|
PreviousNames []string
|
||||||
|
|
||||||
// Pubkey should contain the server's public Ed25519 key.
|
// PublicKey should contain the server's public Ed25519 key.
|
||||||
Pubkey ed25519.PublicKey
|
PublicKey ed25519.PublicKey
|
||||||
// Privkey should contain the server's private Ed25519 key.
|
// PrivateKey should contain the server's private Ed25519 key.
|
||||||
Privkey ed25519.PrivateKey
|
PrivateKey ed25519.PrivateKey
|
||||||
// PreviousKeys should contain any previously-used private keys.
|
// PreviousKeys should contain any previously-used private keys.
|
||||||
// If this is not provided when the key changes, servers will not
|
// If this is not provided when the key changes, servers will not
|
||||||
// trust the new key and all responses will be rejected.
|
// trust the new key and all responses will be rejected.
|
||||||
@@ -32,7 +32,7 @@ type ServerInfoHandler struct {
|
|||||||
type serverInfoData struct {
|
type serverInfoData struct {
|
||||||
ServerName string `json:"server_name"`
|
ServerName string `json:"server_name"`
|
||||||
PreviousNames []string `json:"previous_names"`
|
PreviousNames []string `json:"previous_names"`
|
||||||
Pubkey string `json:"pubkey"`
|
PublicKey string `json:"pubkey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements the http.Handler interface
|
// ServeHTTP implements the http.Handler interface
|
||||||
@@ -40,7 +40,7 @@ func (sih ServerInfoHandler) ServeHTTP(res http.ResponseWriter, req *http.Reques
|
|||||||
data, err := json.Marshal(serverInfoData{
|
data, err := json.Marshal(serverInfoData{
|
||||||
ServerName: sih.ServerName,
|
ServerName: sih.ServerName,
|
||||||
PreviousNames: sih.PreviousNames,
|
PreviousNames: sih.PreviousNames,
|
||||||
Pubkey: base64.StdEncoding.EncodeToString(sih.Pubkey),
|
PublicKey: base64.StdEncoding.EncodeToString(sih.PublicKey),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sih.ErrorHandler(err, res)
|
sih.ErrorHandler(err, res)
|
||||||
@@ -52,9 +52,10 @@ func (sih ServerInfoHandler) ServeHTTP(res http.ResponseWriter, req *http.Reques
|
|||||||
res.Header().Add("X-ProfileFed-Previous", base64.StdEncoding.EncodeToString(sig))
|
res.Header().Add("X-ProfileFed-Previous", base64.StdEncoding.EncodeToString(sig))
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := ed25519.Sign(sih.Privkey, data)
|
sig := ed25519.Sign(sih.PrivateKey, data)
|
||||||
res.Header().Set("X-ProfileFed-Sig", base64.StdEncoding.EncodeToString(sig))
|
res.Header().Set("X-ProfileFed-Sig", base64.StdEncoding.EncodeToString(sig))
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "application/json")
|
||||||
_, err = res.Write(data)
|
_, err = res.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sih.ErrorHandler(err, res)
|
sih.ErrorHandler(err, res)
|
||||||
|
|||||||
Reference in New Issue
Block a user