Compare commits

...

6 Commits

Author SHA1 Message Date
7025f0abbb Fix non-pointer unmarshal 2024-03-17 19:16:43 -07:00
7161f649fa Add LookupWebFinger functions 2024-03-17 18:28:46 -07:00
b3bae0bf86 Remove serverInfoReq function 2024-03-10 16:03:55 -07:00
67c05fd4a2 Verify previous names in client 2024-03-10 16:00:30 -07:00
526b73aa99 Fix LookupAll functions 2024-03-10 15:34:40 -07:00
2c3e0b3df6 Add LookupID/LookupAll client functions 2024-03-10 15:25:36 -07:00

192
client.go
View File

@@ -62,103 +62,190 @@ type Client struct {
GetPubkey func(serverName string) (ed25519.PublicKey, error)
}
// Lookup looks up the profile descriptor for the given ID.
func (c Client) Lookup(id string) (*Descriptor, error) {
wfdesc, err := webfinger.LookupAcct(id)
// Lookup looks up the profile descriptor for the given resource.
func (c Client) Lookup(resource string) (*Descriptor, error) {
wfdesc, err := webfinger.LookupAcct(resource)
if err != nil {
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")
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)
if err != nil {
return nil, err
return err
}
pubkeySaved := false
pubkey, err := c.GetPubkey(pfdURL.Host)
if errors.Is(err, ErrPubkeyNotFound) {
info, _, err := getServerInfo(pfdURL.Scheme, pfdURL.Host)
data, sig, prevSigs, err := getServerInfo(pfdURL.Scheme, pfdURL.Host)
if err != nil {
return nil, err
return err
}
var info serverInfoData
err = json.Unmarshal(data, &info)
if err != nil {
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 nil, err
return err
}
err = c.SavePubkey(pfdURL.Host, info.PreviousNames, pubkey)
if err != nil {
return nil, err
return err
}
pubkeySaved = true
} 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 {
return nil, err
return err
}
defer res.Body.Close()
if err := checkResp(res, "getProfileDescriptor"); err != nil {
return nil, err
return err
}
data, err := io.ReadAll(io.LimitReader(res.Body, responseSizeLimit))
if err != nil {
return nil, err
return err
}
if err := res.Body.Close(); err != nil {
return nil, err
return err
}
sig, err := getSignature(res)
if err != nil {
return nil, err
return err
}
if !ed25519.Verify(pubkey, data, sig) {
// If the pubkey was just saved in the current request, we probably
// already have the newest one, so just return a mismatch error.
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 {
return nil, err
}
serverData, err := io.ReadAll(io.LimitReader(res.Body, responseSizeLimit))
if err != nil {
return nil, err
return err
}
var info serverInfoData
err = json.Unmarshal(serverData, &info)
if err != nil {
return nil, err
return err
}
newPubkey, err := base64.StdEncoding.DecodeString(info.PublicKey)
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) {
return nil, ErrSignatureMismatch
return ErrSignatureMismatch
}
verified := false
sigs := getPrevSignatures(res)
for _, sig := range sigs {
if ed25519.Verify(pubkey, serverData, sig) {
verified = true
@@ -167,66 +254,51 @@ func (c Client) Lookup(id string) (*Descriptor, error) {
}
if !verified {
return nil, ErrSignatureMismatch
}
infoSig, err := getSignature(res)
if err != nil {
return nil, err
return ErrSignatureMismatch
}
if !ed25519.Verify(newPubkey, infoSig, serverData) {
return nil, ErrSignatureMismatch
return ErrSignatureMismatch
}
err = c.SavePubkey(pfdURL.Host, info.PreviousNames, newPubkey)
if err != nil {
return nil, err
return err
}
if !ed25519.Verify(newPubkey, data, sig) {
return nil, ErrSignatureMismatch
return ErrSignatureMismatch
}
}
desc := &Descriptor{}
err = json.Unmarshal(data, desc)
if err != nil {
return nil, err
return json.Unmarshal(data, dest)
}
if desc.Role == "" {
desc.Role = RoleUser
}
return desc, nil
}
// serverInfoReq performs an HTTP request to retrieve server information.
func serverInfoReq(scheme, host string) (*http.Response, error) {
// getServerInfo retrieves server information.
func getServerInfo(scheme, host string) (data, sig []byte, prevSigs [][]byte, err error) {
serverInfoURL := url.URL{
Scheme: scheme,
Host: host,
Path: "/_profilefed/server",
}
return http.Get(serverInfoURL.String())
}
// getServerInfo retrieves server information.
func getServerInfo(scheme, host string) (serverInfoData, [][]byte, error) {
res, err := serverInfoReq(scheme, host)
res, err := http.Get(serverInfoURL.String())
if err != nil {
return serverInfoData{}, nil, err
return nil, nil, nil, err
}
defer res.Body.Close()
if err := checkResp(res, "getServerInfo"); err != nil {
return serverInfoData{}, nil, err
return nil, nil, nil, err
}
var out serverInfoData
err = json.NewDecoder(io.LimitReader(res.Body, responseSizeLimit)).Decode(&out)
return out, getPrevSignatures(res), err
sig, err = getSignature(res)
if err != nil {
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.