keyproofs/pkg/keyproofs/opengpg.go
2020-12-03 12:32:24 -07:00

218 lines
4.8 KiB
Go

package keyproofs
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/crypto/openpgp/packet"
"github.com/tv42/zbase32"
"golang.org/x/crypto/openpgp/armor"
)
func getOpenPGPkey(ctx context.Context, id string) (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, 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)
}
type EntityKey string
func (k EntityKey) Key() interface{} {
return k
}
type Entity struct {
Primary *mail.Address
SelfSignature *packet.Signature
Emails []*mail.Address
Fingerprint string
Proofs []string
ArmorText string
entity *openpgp.Entity
}
func (e *Entity) Serialize(f io.Writer) error {
return e.entity.Serialize(f)
}
func getEntity(lis openpgp.EntityList) (*Entity, error) {
entity := &Entity{}
var err error
for _, e := range lis {
if e == nil {
continue
}
if e.PrimaryKey == nil {
continue
}
entity.entity = e
entity.Fingerprint = fmt.Sprintf("%X", e.PrimaryKey.Fingerprint)
for name, ident := range e.Identities {
// Pick first identity
if entity.Primary == nil {
entity.Primary, err = mail.ParseAddress(name)
if err != nil {
return entity, err
}
}
// If one is marked primary use that
if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
entity.Primary, err = mail.ParseAddress(name)
if err != nil {
return entity, err
}
} else {
var email *mail.Address
if email, err = mail.ParseAddress(name); err != nil {
return entity, err
}
if email.Address != entity.Primary.Address {
entity.Emails = append(entity.Emails, email)
}
}
// If identity is self signed read notation data.
if ident.SelfSignature != nil && ident.SelfSignature.NotationData != nil {
entity.SelfSignature = ident.SelfSignature
// Get proofs and append to list.
if proofs, ok := ident.SelfSignature.NotationData["proof@metacode.biz"]; ok {
entity.Proofs = append(entity.Proofs, proofs...)
}
}
}
break
}
if entity.Primary == nil {
entity.Primary, _ = mail.ParseAddress("nobody@nodomain.xyz")
}
return entity, err
}
func ReadKey(r io.Reader, useArmored bool) (e *Entity, err error) {
var buf bytes.Buffer
var w io.Writer = &buf
e = &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 = getEntity(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)
}