diff options
| author | Fuwn <[email protected]> | 2026-02-26 15:41:45 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-26 15:41:45 -0800 |
| commit | fec9114caaa7d274e524793d5eb0cf2ef2c5af11 (patch) | |
| tree | 0897394ccdfaf6633e1a4ca8eb02bff49bb93c00 /internal/verify/verifier.go | |
| parent | feat: add read-only PLC API compatibility endpoints (diff) | |
| download | plutia-test-fec9114caaa7d274e524793d5eb0cf2ef2c5af11.tar.xz plutia-test-fec9114caaa7d274e524793d5eb0cf2ef2c5af11.zip | |
feat: Apply Iku formatting
Diffstat (limited to 'internal/verify/verifier.go')
| -rw-r--r-- | internal/verify/verifier.go | 67 |
1 files changed, 66 insertions, 1 deletions
diff --git a/internal/verify/verifier.go b/internal/verify/verifier.go index ae648d3..4cb6b38 100644 --- a/internal/verify/verifier.go +++ b/internal/verify/verifier.go @@ -13,7 +13,6 @@ import ( "fmt" "math/big" "strings" - "github.com/Fuwn/plutia/internal/config" "github.com/Fuwn/plutia/internal/types" "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -38,6 +37,7 @@ func (v *Verifier) ShouldVerify(existing *types.StateV1, seq uint64) bool { if existing == nil { return true } + return seq >= existing.LatestOpSeq default: return true @@ -53,36 +53,49 @@ func (v *Verifier) VerifyOperation(op types.ParsedOperation, existing *types.Sta if op.Prev == "" { return errors.New("missing prev on non-genesis operation") } + if op.Prev != existing.ChainTipHash { return fmt.Errorf("prev linkage mismatch: got %s want %s", op.Prev, existing.ChainTipHash) } } sig, ok := findString(op.Payload, "sig", "signature") + if !ok || strings.TrimSpace(sig) == "" { return errors.New("missing signature") } + pubKeys := make([]string, 0, 8) + if existing != nil && len(existing.RotationKeys) > 0 { pubKeys = append(pubKeys, existing.RotationKeys...) } + pubKeys = append(pubKeys, extractPublicKeys(op.Payload)...) + if len(pubKeys) == 0 { return errors.New("missing verification public key") } + sigBytes, err := decodeFlexible(sig) + if err != nil { return fmt.Errorf("decode signature: %w", err) } + payload, err := signaturePayload(op.Payload) + if err != nil { return err } + for _, key := range pubKeys { vk, err := decodePublicKey(key) + if err != nil { continue } + switch vk.Algo { case "ed25519": if ed25519.Verify(vk.Ed25519, payload, sigBytes) { @@ -98,18 +111,21 @@ func (v *Verifier) VerifyOperation(op types.ParsedOperation, existing *types.Sta } } } + return errors.New("signature verification failed") } func signaturePayload(m map[string]any) ([]byte, error) { if raw, ok := findString(m, "sigPayload", "signaturePayload"); ok && raw != "" { decoded, err := decodeFlexible(raw) + if err == nil && json.Valid(decoded) { return types.CanonicalizeJSON(decoded) } } clone := make(map[string]any, len(m)) + for k, v := range m { switch k { case "sig", "signature", "sigPayload", "signaturePayload": @@ -118,14 +134,19 @@ func signaturePayload(m map[string]any) ([]byte, error) { clone[k] = v } } + encMode, err := cbor.CanonicalEncOptions().EncMode() + if err != nil { return nil, fmt.Errorf("init canonical cbor encoder: %w", err) } + b, err := encMode.Marshal(clone) + if err != nil { return nil, fmt.Errorf("marshal signature payload cbor: %w", err) } + return b, nil } @@ -135,6 +156,7 @@ func findString(m map[string]any, keys ...string) (string, bool) { return v, true } } + return "", false } @@ -143,15 +165,19 @@ func extractPublicKeys(payload map[string]any) []string { seen := map[string]struct{}{} add := func(v string) { v = strings.TrimSpace(v) + if v == "" { return } + if _, ok := seen[v]; ok { return } + seen[v] = struct{}{} out = append(out, v) } + if arr, ok := payload["rotationKeys"].([]any); ok { for _, v := range arr { if s, ok := v.(string); ok { @@ -159,19 +185,23 @@ func extractPublicKeys(payload map[string]any) []string { } } } + if v, ok := findString(payload, "publicKey", "verificationMethod", "signingKey", "recoveryKey"); ok { add(v) } + if vm, ok := payload["verificationMethods"].(map[string]any); ok { if v, ok := vm["atproto"].(string); ok { add(v) } + for _, anyV := range vm { if s, ok := anyV.(string); ok { add(s) } } } + return out } @@ -185,41 +215,54 @@ type verificationKey struct { func decodePublicKey(value string) (verificationKey, error) { if strings.HasPrefix(value, "did:key:") { mb := strings.TrimPrefix(value, "did:key:") + if mb == "" || mb[0] != 'z' { return verificationKey{}, errors.New("did:key must be multibase base58btc") } + decoded, err := base58.Decode(mb[1:]) + if err != nil { return verificationKey{}, fmt.Errorf("decode did:key base58: %w", err) } + if len(decoded) < 3 { return verificationKey{}, errors.New("invalid did:key length") } + code, n := binary.Uvarint(decoded) + if n <= 0 || n >= len(decoded) { return verificationKey{}, errors.New("invalid did:key multicodec prefix") } + keyBytes := decoded[n:] + switch code { case 0xED: if len(keyBytes) != ed25519.PublicKeySize { return verificationKey{}, errors.New("invalid did:key ed25519 length") } + return verificationKey{ Algo: "ed25519", Ed25519: ed25519.PublicKey(keyBytes), }, nil case 0xE7: pub, err := secp256k1.ParsePubKey(keyBytes) + if err != nil { return verificationKey{}, fmt.Errorf("parse secp256k1 did:key: %w", err) } + return verificationKey{Algo: "secp256k1", Secp256k1: pub}, nil case 0x1200: x, y := elliptic.UnmarshalCompressed(elliptic.P256(), keyBytes) + if x == nil || y == nil { return verificationKey{}, errors.New("parse p256 did:key: invalid compressed key") } + return verificationKey{ Algo: "p256", P256: &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, @@ -228,23 +271,30 @@ func decodePublicKey(value string) (verificationKey, error) { return verificationKey{}, errors.New("unsupported did:key multicodec") } } + b, err := decodeFlexible(value) + if err != nil { return verificationKey{}, fmt.Errorf("decode public key: %w", err) } + if len(b) == ed25519.PublicKeySize { return verificationKey{Algo: "ed25519", Ed25519: ed25519.PublicKey(b)}, nil } + pub, err := secp256k1.ParsePubKey(b) + if err == nil { return verificationKey{Algo: "secp256k1", Secp256k1: pub}, nil } + if x, y := elliptic.UnmarshalCompressed(elliptic.P256(), b); x != nil && y != nil { return verificationKey{ Algo: "p256", P256: &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, }, nil } + return verificationKey{}, fmt.Errorf("invalid public key length/type: %d", len(b)) } @@ -252,20 +302,28 @@ func verifySecp256k1(pub *secp256k1.PublicKey, payload, sig []byte) bool { if pub == nil { return false } + var parsed *secpECDSA.Signature + if len(sig) == 64 { var r, s secp256k1.ModNScalar + r.SetByteSlice(sig[:32]) s.SetByteSlice(sig[32:]) + parsed = secpECDSA.NewSignature(&r, &s) } else { der, err := secpECDSA.ParseDERSignature(sig) + if err != nil { return false } + parsed = der } + sum := sha256.Sum256(payload) + return parsed.Verify(sum[:], pub) } @@ -273,12 +331,16 @@ func verifyP256(pub *ecdsa.PublicKey, payload, sig []byte) bool { if pub == nil { return false } + sum := sha256.Sum256(payload) + if len(sig) == 64 { r := new(big.Int).SetBytes(sig[:32]) s := new(big.Int).SetBytes(sig[32:]) + return ecdsa.Verify(pub, sum[:], r, s) } + return ecdsa.VerifyASN1(pub, sum[:], sig) } @@ -286,11 +348,14 @@ func decodeFlexible(v string) ([]byte, error) { if b, err := base64.RawURLEncoding.DecodeString(v); err == nil { return b, nil } + if b, err := base64.StdEncoding.DecodeString(v); err == nil { return b, nil } + if b, err := hex.DecodeString(v); err == nil { return b, nil } + return nil, errors.New("unsupported encoding") } |