139 lines
3.1 KiB
Go
139 lines
3.1 KiB
Go
package opgp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/sour-is/crypto/openpgp"
|
|
"github.com/sour-is/keyproofs/pkg/opgp/entity"
|
|
"github.com/tv42/zbase32"
|
|
"golang.org/x/crypto/openpgp/armor"
|
|
)
|
|
|
|
func GetKey(ctx context.Context, id string) (entity *entity.Entity, err error) {
|
|
if isFingerprint(id) {
|
|
addr := "https://keys.openpgp.org/vks/v1/by-fingerprint/" + strings.ToUpper(id)
|
|
return getEntityHTTP(ctx, addr, true)
|
|
} else if email, err := mail.ParseAddress(id); err == nil {
|
|
addr, advAddr := getWKDPubKeyAddr(email)
|
|
req, err := getEntityHTTP(ctx, addr, false)
|
|
if err == nil {
|
|
return req, err
|
|
}
|
|
|
|
req, err = getEntityHTTP(ctx, advAddr, false)
|
|
if err == nil {
|
|
return req, err
|
|
}
|
|
|
|
addr = "https://keys.openpgp.org/vks/v1/by-email/" + url.QueryEscape(id)
|
|
return getEntityHTTP(ctx, addr, true)
|
|
} else {
|
|
return entity, fmt.Errorf("Parse address: %w", err)
|
|
}
|
|
}
|
|
|
|
func getEntityHTTP(ctx context.Context, url string, useArmored bool) (entity *entity.Entity, err error) {
|
|
log := log.Ctx(ctx)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return entity, err
|
|
}
|
|
cl := http.Client{}
|
|
resp, err := cl.Do(req)
|
|
if err != nil {
|
|
return entity, fmt.Errorf("Requesting key: %w\nRemote URL: %v", err, url)
|
|
}
|
|
log.Debug().
|
|
Bool("useArmored", useArmored).
|
|
Str("status", resp.Status).
|
|
Str("url", url).
|
|
Msg("getEntityHTTP")
|
|
|
|
if resp.StatusCode != 200 {
|
|
return entity, fmt.Errorf("bad response from remote: %s\nRemote URL: %v", resp.Status, url)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.Header.Get("Content-Type") == "application/pgp-keys" {
|
|
useArmored = true
|
|
}
|
|
|
|
return ReadKey(resp.Body, useArmored)
|
|
}
|
|
|
|
func ReadKey(r io.Reader, useArmored bool) (e *entity.Entity, err error) {
|
|
var buf bytes.Buffer
|
|
|
|
var w io.Writer = &buf
|
|
e = &entity.Entity{}
|
|
|
|
defer func() {
|
|
if e != nil {
|
|
e.ArmorText = buf.String()
|
|
}
|
|
}()
|
|
|
|
if !useArmored {
|
|
var aw io.WriteCloser
|
|
aw, err = armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", nil)
|
|
if err != nil {
|
|
return e, fmt.Errorf("Read key: %w", err)
|
|
}
|
|
defer aw.Close()
|
|
|
|
w = aw
|
|
}
|
|
|
|
r = io.TeeReader(r, w)
|
|
|
|
var lis openpgp.EntityList
|
|
|
|
if useArmored {
|
|
lis, err = openpgp.ReadArmoredKeyRing(r)
|
|
} else {
|
|
lis, err = openpgp.ReadKeyRing(r)
|
|
}
|
|
if err != nil {
|
|
return e, fmt.Errorf("Read key: %w", err)
|
|
}
|
|
|
|
e, err = entity.GetOne(lis)
|
|
if err != nil {
|
|
return e, fmt.Errorf("Parse key: %w", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func isFingerprint(s string) bool {
|
|
for _, r := range s {
|
|
switch r {
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func getWKDPubKeyAddr(email *mail.Address) (string, string) {
|
|
parts := strings.SplitN(email.Address, "@", 2)
|
|
hash := sha1.Sum([]byte(parts[0]))
|
|
lp := zbase32.EncodeToString(hash[:])
|
|
|
|
return fmt.Sprintf("https://%s/.well-known/openpgpkey/hu/%s", parts[1], lp),
|
|
fmt.Sprintf("https://openpgpkey.%s/.well-known/openpgpkey/hu/%s/%s", parts[1], parts[1], lp)
|
|
}
|