diff options
Diffstat (limited to 'internal/identity/verify.go')
| -rw-r--r-- | internal/identity/verify.go | 92 |
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 +} |
