refactor out into packages for easier unit test writing
This commit is contained in:
89
pkg/opgp/entity/entity.go
Normal file
89
pkg/opgp/entity/entity.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
|
||||
"github.com/sour-is/crypto/openpgp"
|
||||
"github.com/sour-is/crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
type Key string
|
||||
|
||||
func (k Key) 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 GetOne(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
|
||||
}
|
||||
138
pkg/opgp/util.go
Normal file
138
pkg/opgp/util.go
Normal file
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user