summaryrefslogtreecommitdiffstats
path: root/internal/identity/verify.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/identity/verify.go')
-rw-r--r--internal/identity/verify.go92
1 files changed, 92 insertions, 0 deletions
diff --git a/internal/identity/verify.go b/internal/identity/verify.go
new file mode 100644
index 0000000..e337e07
--- /dev/null
+++ b/internal/identity/verify.go
@@ -0,0 +1,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
+}