summaryrefslogtreecommitdiffstats
path: root/internal/identity/verify.go
blob: e337e070b9b4ba5cfc1ef4fd50c24ca12e0e51f6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package identity

import (
	"crypto/ed25519"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/mail"
	"strings"

	"aegis/internal/cryptokit"
)

type VerificationResult struct {
	VFaceValid        bool
	IdentityHashValid bool
	FaceValid         bool
	SignaturePresent  bool
	SignatureValid    bool
	PublicKey         string
	IdentityHash      string
	PNGHash           string
	Error             error
}

func VerifyArticle(article string) VerificationResult {
	message, err := mail.ReadMessage(strings.NewReader(article))
	if err != nil {
		return VerificationResult{Error: fmt.Errorf("parse article: %w", err)}
	}
	from, err := mail.ParseAddress(message.Header.Get("From"))
	if err != nil {
		return VerificationResult{Error: fmt.Errorf("parse From: %w", err)}
	}
	publicKey := strings.TrimSpace(message.Header.Get("X-Ed25519-Pub"))
	result := VerificationResult{PublicKey: publicKey, IdentityHash: strings.TrimSpace(message.Header.Get("Identity-Hash")), PNGHash: strings.TrimSpace(message.Header.Get("X-VFace-PNG-SHA256"))}
	profile, profileErr := GenerateVFace(from.Name, from.Address, publicKey)
	if profileErr != nil {
		result.Error = profileErr
		return result
	}
	result.IdentityHashValid = strings.EqualFold(strings.TrimPrefix(result.IdentityHash, "sha256:"), profile.IdentityHash)
	result.FaceValid = strings.TrimSpace(message.Header.Get("Face")) == profile.FaceBase64
	result.VFaceValid = result.IdentityHashValid && result.FaceValid
	if result.PNGHash != "" && !strings.EqualFold(strings.TrimPrefix(result.PNGHash, "sha256:"), profile.PNGHash) {
		result.VFaceValid = false
	}
	signature := strings.TrimSpace(message.Header.Get("X-Ed25519-Sig"))
	if signature == "" {
		value := strings.TrimSpace(message.Header.Get("X-Aegis-Signature"))
		if parts := strings.SplitN(value, ";", 2); len(parts) == 2 && strings.EqualFold(strings.TrimSpace(parts[0]), "ed25519") {
			signature = strings.TrimSpace(parts[1])
		}
	}
	if signature == "" {
		return result
	}
	result.SignaturePresent = true
	signatureBytes, decodeErr := base64.StdEncoding.DecodeString(signature)
	if decodeErr != nil || len(signatureBytes) != ed25519.SignatureSize {
		result.Error = errors.New("invalid Ed25519 signature encoding")
		return result
	}
	body, readErr := io.ReadAll(io.LimitReader(message.Body, 16<<20))
	if readErr != nil {
		result.Error = fmt.Errorf("read article body: %w", readErr)
		return result
	}
	key, keyErr := decodePublicKey(publicKey)
	if keyErr != nil {
		result.Error = keyErr
		return result
	}
	result.SignatureValid = ed25519.Verify(key, body, signatureBytes)
	if !result.SignatureValid {
		result.Error = errors.New("Ed25519 signature verification failed")
	}
	return result
}

func decodePublicKey(value string) (ed25519.PublicKey, error) {
	canonicalText, err := cryptokit.CanonicalEd25519PublicKey(value)
	if err != nil {
		return nil, err
	}
	canonical, err := base64.StdEncoding.DecodeString(canonicalText)
	if err != nil {
		return nil, fmt.Errorf("decode Ed25519 public key: %w", err)
	}
	return ed25519.PublicKey(canonical), nil
}