package securechat // Ejecuta packages/crypto/vectors/v1.json con las mismas comprobaciones que // packages/crypto/src/run-vectors.js, y los casos HMAC de ../../fixtures/signing.json. // // go test -v ./... → "v1.json: 82/82" y "signing.json: 17/17" import ( "bytes" "crypto/ed25519" "encoding/json" "errors" "fmt" "os" "path/filepath" "testing" ) type vKey struct{ Seed, PublicKey, PrivateKey string } type vectors struct { Keys map[string]vKey `json:"keys"` Conversation struct { ConversationID string `json:"conversationId"` Key string `json:"key"` } `json:"conversation"` Messages []struct { Name, SenderType, ClientMessageID, Text, Nonce, Ad, Ciphertext string KeyVersion, PaddedLength int Envelope Envelope } `json:"messages"` NegativeMessages []struct { Name, ConversationID, SenderType, Key, ExpectError string Envelope Envelope } `json:"negativeMessages"` Wraps []struct { Name, Recipient, ExpectConversationID, ExpectError string Wrapped Wrap } `json:"wraps"` CompanyKey struct { CompanyID, PublicKey, Signature, PlatformPublicKey, Fingerprint string KeyVersion int Negative []struct { Name, CompanyID, PublicKey, Signature string KeyVersion int } } `json:"companyKey"` CompanyRotation struct { vRotation SigningPublicKey, SigningSeed string Negative []vRotation } `json:"companyRotation"` AgentRoster struct { SigningPublicKey string AgentKeys map[string]vKey Valid []vRoster Negative []vRoster } `json:"agentRoster"` AgentWraps []struct { Name, Recipient, ExpectConversationID, ExpectError string Wrapped Wrap } `json:"agentWraps"` Attachments struct { Key, NonceBase string Cases []struct { Name, AttachmentID, CiphertextDigest string Size, CiphertextLength int Meta AttachmentMeta } } `json:"attachments"` InviteBinding struct { InviteSecret, ConversationID, UserPublicKey, Tag string WrapForCompany Wrap } `json:"inviteBinding"` } type vRotation struct { Name, CompanyID, FromPublicKey, ToPublicKey, Signature, ExpectError string FromKeyVersion, ToKeyVersion int } type vRoster struct { Name, CompanyID, IssuedAt, Statement, Signature, ExpectError string RosterVersion int Agents []struct{ AgentKeyID, PublicKey string } } func (x vRoster) roster() AgentRoster { r := AgentRoster{CompanyID: x.CompanyID, RosterVersion: x.RosterVersion, IssuedAt: x.IssuedAt, Agents: []AgentKey{}} for _, a := range x.Agents { r.Agents = append(r.Agents, AgentKey{a.AgentKeyID, mustB64(a.PublicKey)}) } return r } type signingFixtures struct { Secret string Webhook struct { Now, ToleranceSeconds int64 Cases []struct { Name, RawBody, Header string ExpectError *string } } Requests []struct { Name, Method, PathAndQuery, RawBody, ExpectedHeader string Timestamp int64 } } type result struct { name, detail string pass bool } type runner struct{ results []result } func (r *runner) check(name string, fn func() (string, error)) { detail, err := fn() if err != nil { r.results = append(r.results, result{name, err.Error(), false}) return } r.results = append(r.results, result{name, detail, true}) } func expectCode(err error, code string) (string, error) { if err == nil { return "", fmt.Errorf("se esperaba %s, no falló", code) } if got := ErrorCode(err); got != code { return "", fmt.Errorf("se esperaba %s, falló con %q (%v)", code, got, err) } return "falló con " + code, nil } func (r *runner) report(t *testing.T, label string) { t.Helper() failed := 0 for _, x := range r.results { mark := "✔" if !x.pass { mark = "✘" failed++ t.Errorf("%s: %s", x.name, x.detail) } t.Logf("%s %s %s", mark, x.name, x.detail) } fmt.Printf("%s: %d/%d\n", label, len(r.results)-failed, len(r.results)) } func mustB64(s string) []byte { b, err := B64uDecode(s) if err != nil { panic(err) } return b } func readJSON(t *testing.T, env, rel string, out any) { t.Helper() path := os.Getenv(env) if path == "" { path = filepath.FromSlash(rel) } raw, err := os.ReadFile(path) if err != nil { t.Fatalf("no se pudo leer %s: %v", path, err) } if err := json.Unmarshal(raw, out); err != nil { t.Fatalf("JSON inválido en %s: %v", path, err) } } func pattern(n int) []byte { b := make([]byte, n) for i := range b { b[i] = byte((i*31 + 7) & 255) } return b } func TestVectorsV1(t *testing.T) { var V vectors readJSON(t, "SECURECHAT_VECTORS", "../../../../packages/crypto/vectors/v1.json", &V) r := &runner{} cid := V.Conversation.ConversationID key := mustB64(V.Conversation.Key) kp := func(who string) KeyPair { return KeyPair{PublicKey: mustB64(V.Keys[who].PublicKey), PrivateKey: mustB64(V.Keys[who].PrivateKey)} } // 1. Derivación determinista X25519 for _, who := range []string{"user", "company", "recoveryDevice"} { r.check("seed-keypair/"+who, func() (string, error) { k, err := IdentityKeyPairFromSeed(mustB64(V.Keys[who].Seed)) if err != nil { return "", err } if B64uEncode(k.PublicKey) != V.Keys[who].PublicKey || B64uEncode(k.PrivateKey) != V.Keys[who].PrivateKey { return "", errors.New("par distinto") } return "", nil }) } // 2. Mensajes for _, m := range V.Messages { r.check("message/"+m.Name, func() (string, error) { ad, err := MessageAD(cid, m.SenderType, m.ClientMessageID, m.KeyVersion) if err != nil || ad != m.Ad { return "", fmt.Errorf("AD distinto: %s %v", ad, err) } env, err := EncryptMessage(key, cid, m.SenderType, m.ClientMessageID, m.KeyVersion, m.Text, mustB64(m.Nonce)) if err != nil { return "", err } if env.Ciphertext != m.Ciphertext { return "", errors.New("ciphertext distinto al del vector") } if len(mustB64(env.Ciphertext)) != m.PaddedLength+16 || PaddedLength(0) != 256 { return "", errors.New("longitud con relleno inesperada") } text, err := DecryptMessage(key, cid, m.SenderType, m.Envelope) if err != nil { return "", err } if text != m.Text { return "", errors.New("texto descifrado distinto") } return "", nil }) } // 3. Mensajes que DEBEN fallar for _, n := range V.NegativeMessages { r.check("negative/"+n.Name, func() (string, error) { k := key if n.Key != "" { k = mustB64(n.Key) } _, err := DecryptMessage(k, n.ConversationID, n.SenderType, n.Envelope) return expectCode(err, n.ExpectError) }) } // 4. Envolturas for _, w := range V.Wraps { r.check("wrap/"+w.Name, func() (string, error) { got, err := OpenConversationKey(w.Wrapped, w.ExpectConversationID, kp(w.Recipient)) if w.ExpectError != "" { return expectCode(err, w.ExpectError) } if err != nil { return "", err } if !bytes.Equal(got, key) { return "", errors.New("K distinta") } return "K correcta", nil }) } // 5. Firma de plataforma ck := V.CompanyKey ppk := mustB64(ck.PlatformPublicKey) r.check("company-key/valid", func() (string, error) { return "firma válida", VerifyCompanyKey(ck.CompanyID, ck.KeyVersion, mustB64(ck.PublicKey), ck.Signature, ppk) }) for _, n := range ck.Negative { r.check("company-key/"+n.Name, func() (string, error) { return expectCode(VerifyCompanyKey(n.CompanyID, n.KeyVersion, mustB64(n.PublicKey), n.Signature, ppk), ErrSignatureInvalid) }) } // 5b. Rotación cr := V.CompanyRotation spk := mustB64(cr.SigningPublicKey) rot := func(x vRotation) error { return Rotation{x.CompanyID, x.FromKeyVersion, mustB64(x.FromPublicKey), x.ToKeyVersion, mustB64(x.ToPublicKey)}.Verify(x.Signature, spk) } r.check("company-rotation/valid", func() (string, error) { return "firma válida", rot(cr.vRotation) }) for _, n := range cr.Negative { r.check("company-rotation/"+n.Name, func() (string, error) { return expectCode(rot(n), n.ExpectError) }) } // 5b'. Lista de agentes firmada por la empresa (misma llave de firma que la rotación) ar := V.AgentRoster arPk := mustB64(ar.SigningPublicKey) arSk := ed25519.NewKeyFromSeed(mustB64(cr.SigningSeed)) for _, c := range ar.Valid { r.check("agent-roster/"+c.Name, func() (string, error) { stmt, err := c.roster().Statement() if err != nil { return "", err } if string(stmt) != c.Statement { return "", fmt.Errorf("enunciado distinto: %s", stmt) } sig, err := c.roster().Sign(arSk) if err != nil { return "", err } if sig != c.Signature { return "", errors.New("firma distinta a la del vector") } return "firma válida", c.roster().Verify(c.Signature, arPk) }) } for _, n := range ar.Negative { r.check("agent-roster/"+n.Name, func() (string, error) { return expectCode(n.roster().Verify(n.Signature, arPk), n.ExpectError) }) } for _, w := range V.AgentWraps { r.check("agent-wrap/"+w.Name, func() (string, error) { k := ar.AgentKeys[w.Recipient] got, err := OpenConversationKey(w.Wrapped, w.ExpectConversationID, KeyPair{PublicKey: mustB64(k.PublicKey), PrivateKey: mustB64(k.PrivateKey)}) if w.ExpectError != "" { return expectCode(err, w.ExpectError) } if err != nil { return "", err } if !bytes.Equal(got, key) { return "", errors.New("K distinta") } return "K correcta", nil }) } // 5c. Adjuntos att := V.Attachments aKey, aNonce := mustB64(att.Key), mustB64(att.NonceBase) for _, c := range att.Cases { r.check("attachment/"+c.Name, func() (string, error) { plain := pattern(c.Size) ct, meta, err := EncryptAttachment(plain, c.AttachmentID, c.Meta.Name, c.Meta.Mime, nil, aKey, aNonce) if err != nil { return "", err } if len(ct) != c.CiphertextLength { return "", errors.New("longitud distinta") } if AttachmentDigest(ct) != c.CiphertextDigest { return "", errors.New("ciphertext distinto al del vector") } a, _ := json.Marshal(meta) b, _ := json.Marshal(c.Meta) if !bytes.Equal(a, b) { return "", errors.New("meta distinta") } text, err := AttachmentMessageText(c.Meta) if err != nil { return "", err } parsed, err := ParseAttachmentMessage(text) if err != nil { return "", err } back, err := DecryptAttachment(ct, parsed) if err != nil { return "", err } if !bytes.Equal(back, plain) { return "", errors.New("descifrado distinto") } return "", nil }) } var big = att.Cases[len(att.Cases)-1] for _, c := range att.Cases { if c.Name == "three-chunks" { big = c } } ct, _, _ := EncryptAttachment(pattern(big.Size), big.AttachmentID, big.Meta.Name, big.Meta.Mime, nil, aKey, aNonce) const CH = 65536 + 16 with := func(f func(m *AttachmentMeta)) AttachmentMeta { m := big.Meta; f(&m); return m } tamper := []struct { name string ct []byte meta AttachmentMeta }{ {"flipped-byte", func() []byte { t := bytes.Clone(ct); t[CH+5] ^= 1; return t }(), big.Meta}, {"truncated-last-chunk", ct[:2*CH], with(func(m *AttachmentMeta) { m.Size = 2 * 65536 })}, {"swapped-chunks", append(append(bytes.Clone(ct[CH:2*CH]), ct[:CH]...), ct[2*CH:]...), big.Meta}, {"other-attachmentId", ct, with(func(m *AttachmentMeta) { m.AttachmentID = "att_vectorOTHER" })}, {"wrong-digest", ct, with(func(m *AttachmentMeta) { m.Digest = att.Cases[0].Meta.Digest })}, {"wrong-size", ct, with(func(m *AttachmentMeta) { m.Size = big.Size - 1 })}, } for _, tc := range tamper { r.check("attachment-negative/"+tc.name, func() (string, error) { _, err := DecryptAttachment(tc.ct, tc.meta) return expectCode(err, ErrDecryptFailed) }) } r.check("attachment-negative/meta-with-slash-in-name", func() (string, error) { m := with(func(m *AttachmentMeta) { m.Name = "../x" }) raw, _ := json.Marshal(m) _, err := ParseAttachmentMessage(string(raw)) return expectCode(err, ErrInvalidInput) }) // 6. Huella r.check("fingerprint/company", func() (string, error) { fp, err := KeyFingerprint(mustB64(ck.PublicKey)) if err != nil { return "", err } if B64uEncode(fp) != ck.Fingerprint { return "", fmt.Errorf("huella distinta: %s", B64uEncode(fp)) } return FormatFingerprint(fp), nil }) // 7. Vinculación con el secreto de la invitación ib := V.InviteBinding r.check("invite-binding/tag", func() (string, error) { tag, err := InviteBindingTag(mustB64(ib.InviteSecret), ib.ConversationID, mustB64(ib.UserPublicKey), ib.WrapForCompany) if err != nil { return "", err } if tag != ib.Tag { return "", errors.New("tag distinto") } return "verifica", VerifyInviteBinding(tag, mustB64(ib.InviteSecret), ib.ConversationID, mustB64(ib.UserPublicKey), ib.WrapForCompany) }) r.check("invite-binding/substituted-user-key", func() (string, error) { return expectCode(VerifyInviteBinding(ib.Tag, mustB64(ib.InviteSecret), ib.ConversationID, mustB64(V.Keys["recoveryDevice"].PublicKey), ib.WrapForCompany), ErrBindingInvalid) }) r.report(t, "v1.json") } func TestSigningFixtures(t *testing.T) { var S signingFixtures readJSON(t, "SECURECHAT_SIGNING_FIXTURES", "../../fixtures/signing.json", &S) r := &runner{} for _, c := range S.Webhook.Cases { r.check("webhook/"+c.Name, func() (string, error) { err := VerifyWebhook([]byte(c.RawBody), c.Header, S.Secret, S.Webhook.ToleranceSeconds, S.Webhook.Now) if c.ExpectError != nil { return expectCode(err, *c.ExpectError) } return "firma válida", err }) } for _, q := range S.Requests { r.check("request/"+q.Name, func() (string, error) { if h := SignRequest(q.Method, q.PathAndQuery, []byte(q.RawBody), S.Secret, q.Timestamp); h != q.ExpectedHeader { return "", fmt.Errorf("cabecera distinta: %s", h) } return "", nil }) } r.report(t, "signing.json") }