diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index e6784c2b2..edcb87256 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -2,7 +2,7 @@ # Copyright 2026 libsignal-go contributors. # SPDX-License-Identifier: AGPL-3.0-only # -# compat-interop: builds the v0.96.4-pinned Rust harness and runs the live +# compat-interop: builds the v0.104.0-pinned Rust harness and runs the live # JSON-RPC interop tests (compat/interop_test.go, build tag `interop`) against # it, asserting the pure-Go implementation agrees with upstream on live # operations. The committed-vector tests run in the `go` workflow with no Rust @@ -55,7 +55,11 @@ jobs: - name: build rust harness (release) working-directory: compat/rust-harness - run: cargo build --release + run: cargo build --release --locked + + - name: replay account-key oracle fixtures + working-directory: compat/rust-harness + run: cargo test --release --locked - name: interop tests env: diff --git a/README.md b/README.md index 187bf22ad..355bd27e3 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,19 @@ target, so that the interop gate is meaningful and reproducible. v0.91.0** protocol surface — the last upstream release before the Sparse Post-Quantum Ratchet (SPQR) was made mandatory for new sessions — while SPQR was being ported (P5–P9 ran against this pin). -- **Stage 2 (current):** SPQR is ported (P10) and the compat harness is - re-pinned to **libsignal v0.96.4**, the current upstream mainline release. The - compatibility claim now covers the full current-mainline protocol surface, +- **Protocol baseline:** SPQR is ported and the compatibility baseline is + **libsignal v0.96.4**. The + compatibility claim covers that pinned protocol surface, including SPQR-negotiated sessions (v0.96.4 requires SPQR — `min_version: V1`). The committed vectors are byte-identical across the re-pin (the protos, the `spqr` v1.5.1 pin, and `libcrux-ml-kem` 0.0.8 are unchanged from v0.91.0), and the live Rust↔Go interop passes both roles with SPQR on the wire. +- **Current oracle:** the development-only Rust harness is pinned to the + immutable **libsignal v0.104.0** commit, with SPQR 1.6.0 and libcrux 0.0.10. + Account-key, PIN master-key, media-key, and MFA metadata vectors are refreshed + against this pin. The remaining committed vectors and global proof-report + baseline retain their original provenance until the full parity refresh; + changing the oracle dependency is not itself a full protocol upgrade. The rationale, alternatives considered, and the exact pin boundary are recorded in [`decisions/0001-spqr-staged-compat.md`](decisions/0001-spqr-staged-compat.md). @@ -130,7 +136,7 @@ deliberate non-goals for this module. | AES-256-GCM-SIV (RFC 8452) | ✅ implemented | [`internal/crypto/gcmsiv`](internal/crypto/gcmsiv/) | nonce-misuse-resistant AEAD for sealed sender v2 | | Fingerprints (numeric + scannable) | ✅ implemented | [`fingerprint`](fingerprint/) | display + scannable byte-equal vs upstream | | Sparse Post-Quantum Ratchet (SPQR) | ✅ implemented | [`spqr`](spqr/), [`internal/mlkem768incr`](internal/mlkem768incr/), [`internal/spqr/chunked`](internal/spqr/chunked/) | incremental ML-KEM-768 + GF(2^16) chunked transport + state machine, mixed into the session message keys; SPQR-negotiated interop both roles at v0.96.4 | -| Account keys (entropy pool, SVR key, PIN hash, backup key derivations) | ✅ implemented | [`accountkeys`](accountkeys/) | v0.96.4 known vectors for account entropy, backup ID, PIN hash, and local PIN PHC | +| Account keys (entropy pool, SVR/PIN, MFA metadata, backup derivations) | ✅ implemented | [`accountkeys`](accountkeys/) | v0.104.0 Rust oracle for SVR derivatives, PIN master-key SIV, fixed 160-byte MFA metadata, and media AES/HMAC splits; legacy entropy/backup/PIN vectors preserved | | Username validation, candidates, username links, and reservation hash | ✅ implemented | [`usernames`](usernames/) | v0.96.4 username-link and reserve-hash vectors; proof generation deferred to zk/poksho phase | | X3DH v3 session *initiation* | ⛔ excluded | — | v3 *decrypt*/state compat retained; v0.96.4 cannot initiate v3 | | ML-KEM-1024 *activation* | ⛔ excluded | — | wire type `0x0A` parsing reserved only | @@ -191,8 +197,14 @@ Runnable examples live alongside the packages they document (Go renders them in distribution and a group-encrypted message. - [`sealedsender.Example_sealedSender`](sealedsender/example_test.go) — a sealed sender v1 message with certificate-chain validation. -- [`accountkeys`](accountkeys/) — account entropy, SVR key, PIN hash, and backup - key derivations. +- [`accountkeys`](accountkeys/) — account entropy, redacted `SVRKey`, registration + lock/recovery/storage/logging derivatives, authenticated PIN master-key + encoding, fixed-size encrypted MFA metadata, and backup/media key derivations. + MFA timestamps round down to whole seconds on the wire; names allow at most + 98 UTF-8 bytes and no NUL. `Encrypt` requires a cryptographic random reader + such as `crypto/rand.Reader`. These are local cryptographic operations, not + registration, account recovery, or MFA enrollment APIs. Temporary buffers are + cleared on a best-effort basis; Go does not guarantee complete key zeroization. - [`usernames`](usernames/) — username validation, candidate generation, username links, and vector-backed reservation hashes via `Hash`, `HashFromParts`, `HashHex`, and `CandidatesWithHashes`. diff --git a/accountkeys/accountkeys.go b/accountkeys/accountkeys.go index f92c0ff9a..c04c22bef 100644 --- a/accountkeys/accountkeys.go +++ b/accountkeys/accountkeys.go @@ -1,6 +1,7 @@ // Package accountkeys implements Signal account-level key derivations. // -// It is a pure-Go port of upstream libsignal rust/account-keys at v0.96.4. +// It is a pure-Go port of upstream libsignal rust/account-keys, including +// the SVR/PIN and MFA metadata additions verified against v0.104.0. // The package deliberately contains no network or storage behavior. package accountkeys @@ -39,6 +40,10 @@ const ( BackupForwardSecrecyTokenLen = 32 // MediaEncryptionKeyLen is the length in bytes of backup media encryption key data. MediaEncryptionKeyLen = 64 + // MediaEncryptionAESKeyLen is the AES key prefix length in media encryption key data. + MediaEncryptionAESKeyLen = 32 + // MediaEncryptionHMACKeyLen is the HMAC key suffix length in media encryption key data. + MediaEncryptionHMACKeyLen = 32 ) var ( diff --git a/accountkeys/mfa_metadata.go b/accountkeys/mfa_metadata.go new file mode 100644 index 000000000..b5e1e0628 --- /dev/null +++ b/accountkeys/mfa_metadata.go @@ -0,0 +1,209 @@ +package accountkeys + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/subtle" + "errors" + "fmt" + "io" + "math" + "strings" + "unicode/utf8" + + "github.com/GoCodeAlone/libsignal-go/internal/accountkeysproto" + "google.golang.org/protobuf/encoding/protowire" + "google.golang.org/protobuf/proto" +) + +const ( + // MFAMetadataCiphertextLen is the fixed size of encrypted MFA key metadata. + MFAMetadataCiphertextLen = 160 + // MFAKeyNameMaxLen is the maximum MFA key name length in UTF-8 bytes. + MFAKeyNameMaxLen = 98 + + mfaPaddedPlaintextLen = 112 + mfaAuthenticatedLen = aes.BlockSize + mfaPaddedPlaintextLen +) + +var errInvalidMFAMetadata = errors.New("invalid MFA metadata") + +// MFAMetadata describes an MFA key without any service or account operations. +type MFAMetadata struct { + name string + createdAtEpochMillis uint64 +} + +// EncryptedMFAMetadata is the opaque, authenticated fixed-size MFA metadata blob. +type EncryptedMFAMetadata [MFAMetadataCiphertextLen]byte + +// NewMFAMetadata validates metadata for an MFA key. +func NewMFAMetadata(name string, createdAtEpochMillis uint64) (MFAMetadata, error) { + if !utf8.ValidString(name) || strings.ContainsRune(name, 0) || len(name) > MFAKeyNameMaxLen { + return MFAMetadata{}, errors.New("invalid MFA key name") + } + return MFAMetadata{name: name, createdAtEpochMillis: createdAtEpochMillis}, nil +} + +// Name returns the MFA key's name. +func (m MFAMetadata) Name() string { return m.name } + +// CreatedAtEpochMillis returns the creation time in milliseconds. +func (m MFAMetadata) CreatedAtEpochMillis() uint64 { return m.createdAtEpochMillis } + +// Encrypt authenticates and encrypts metadata using key and a cryptographic rng. +func (m MFAMetadata) Encrypt(key SVRKey, rng io.Reader) (EncryptedMFAMetadata, error) { + if _, err := NewMFAMetadata(m.name, m.createdAtEpochMillis); err != nil { + return EncryptedMFAMetadata{}, err + } + if rng == nil { + return EncryptedMFAMetadata{}, errors.New("MFA metadata requires a cryptographic random source") + } + plaintext, err := proto.Marshal(&accountkeysproto.MfaKeyMetadataPlaintext{ + CreationEpochTimeSeconds: m.createdAtEpochMillis / 1000, + KeyName: m.name, + }) + if err != nil { + return EncryptedMFAMetadata{}, errInvalidMFAMetadata + } + defer clear(plaintext) + if len(plaintext) >= mfaPaddedPlaintextLen { + return EncryptedMFAMetadata{}, errInvalidMFAMetadata + } + // Upstream pads to this fixed size, not to the next AES block boundary. + var padded [mfaPaddedPlaintextLen]byte + defer clear(padded[:]) + copy(padded[:], plaintext) + padding := byte(mfaPaddedPlaintextLen - len(plaintext)) // #nosec G115 -- length guard bounds padding to 1..112. + for i := len(plaintext); i < len(padded); i++ { + padded[i] = padding + } + var out EncryptedMFAMetadata + if _, err := io.ReadFull(rng, out[:aes.BlockSize]); err != nil { + return EncryptedMFAMetadata{}, fmt.Errorf("MFA metadata randomness: %w", err) + } + cipherKey, authKey := key.mfaMetadataKeys() + defer clear(cipherKey[:]) + defer clear(authKey[:]) + block, err := aes.NewCipher(cipherKey[:]) + if err != nil { + return EncryptedMFAMetadata{}, errInvalidMFAMetadata + } + cipher.NewCBCEncrypter(block, out[:aes.BlockSize]).CryptBlocks(out[aes.BlockSize:mfaAuthenticatedLen], padded[:]) + mac := keyedHash(authKey[:], out[:mfaAuthenticatedLen]) + copy(out[mfaAuthenticatedLen:], mac[:]) + clear(mac[:]) + return out, nil +} + +// ParseEncryptedMFAMetadata copies a fixed-size ciphertext from raw. +func ParseEncryptedMFAMetadata(raw []byte) (EncryptedMFAMetadata, error) { + if len(raw) != MFAMetadataCiphertextLen { + return EncryptedMFAMetadata{}, errInvalidMFAMetadata + } + return EncryptedMFAMetadata(raw), nil +} + +// Bytes returns a copy of the encrypted metadata's fixed-width representation. +func (m EncryptedMFAMetadata) Bytes() [MFAMetadataCiphertextLen]byte { return m } + +// Decrypt authenticates and decrypts metadata, rejecting malformed plaintext. +func (m EncryptedMFAMetadata) Decrypt(key SVRKey) (MFAMetadata, error) { + cipherKey, authKey := key.mfaMetadataKeys() + defer clear(cipherKey[:]) + defer clear(authKey[:]) + mac := keyedHash(authKey[:], m[:mfaAuthenticatedLen]) + defer clear(mac[:]) + if !hmac.Equal(mac[:], m[mfaAuthenticatedLen:]) { + return MFAMetadata{}, errInvalidMFAMetadata + } + block, err := aes.NewCipher(cipherKey[:]) + if err != nil { + return MFAMetadata{}, errInvalidMFAMetadata + } + var padded [mfaPaddedPlaintextLen]byte + defer clear(padded[:]) + cipher.NewCBCDecrypter(block, m[:aes.BlockSize]).CryptBlocks(padded[:], m[aes.BlockSize:mfaAuthenticatedLen]) + padding := int(padded[len(padded)-1]) + if padding == 0 || padding > len(padded) { + return MFAMetadata{}, errInvalidMFAMetadata + } + valid := 1 + for _, b := range padded[len(padded)-padding:] { + valid &= subtle.ConstantTimeByteEq(b, byte(padding)) + } + if valid != 1 { + return MFAMetadata{}, errInvalidMFAMetadata + } + var plaintext accountkeysproto.MfaKeyMetadataPlaintext + encoded, validWire := mfaProtoPlaintext(padded[:len(padded)-padding]) + defer clear(encoded) + if !validWire { + return MFAMetadata{}, errInvalidMFAMetadata + } + if err := proto.Unmarshal(encoded, &plaintext); err != nil { + return MFAMetadata{}, errInvalidMFAMetadata + } + if plaintext.CreationEpochTimeSeconds > math.MaxUint64/1000 { + return MFAMetadata{}, errInvalidMFAMetadata + } + metadata, err := NewMFAMetadata(plaintext.KeyName, plaintext.CreationEpochTimeSeconds*1000) + if err != nil { + return MFAMetadata{}, errInvalidMFAMetadata + } + return metadata, nil +} + +func (k SVRKey) mfaMetadataKeys() (cipherKey, authKey [32]byte) { + return keyedHash(k.bytes[:], []byte("20260831 TOTP Metadata Encryption")), + keyedHash(k.bytes[:], []byte("20260831 TOTP Metadata Authentication")) +} + +// Rust protobuf 3.7 decodes tags and byte lengths as five-byte u32 varints; +// Go accepts longer encodings. Check those bounds without replacing generated +// decoding. Rust discards unknown groups, accepting any valid end-group tag; +// omit those groups here while retaining ordinary unknown fields. +func mfaProtoPlaintext(raw []byte) ([]byte, bool) { + encoded := make([]byte, 0, len(raw)) + groups := 0 + for len(raw) > 0 { + field := raw + tag, n := protowire.ConsumeVarint(raw) + if n < 0 || n > 5 || tag > math.MaxUint32 { + return encoded, false + } + num, typ := protowire.DecodeTag(tag) + if !num.IsValid() { + return encoded, false + } + raw = raw[n:] + switch typ { + case protowire.StartGroupType: + groups++ + continue + case protowire.EndGroupType: + if groups == 0 { + return encoded, false + } + groups-- + continue + case protowire.BytesType: + length, prefix := protowire.ConsumeVarint(raw) + if prefix < 0 || prefix > 5 || length > math.MaxUint32 || length > uint64(len(raw)-prefix) { + return encoded, false + } + n = prefix + int(length) // #nosec G115 -- length is bounded by the remaining 111-byte plaintext. + default: + n = protowire.ConsumeFieldValue(num, typ, raw) + if n < 0 { + return encoded, false + } + } + if groups == 0 { + encoded = append(encoded, field[:len(field)-len(raw)+n]...) + } + raw = raw[n:] + } + return encoded, groups == 0 +} diff --git a/accountkeys/mfa_metadata_test.go b/accountkeys/mfa_metadata_test.go new file mode 100644 index 000000000..ad29c6bd2 --- /dev/null +++ b/accountkeys/mfa_metadata_test.go @@ -0,0 +1,234 @@ +package accountkeys + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "io" + "math" + "strings" + "testing" + + "google.golang.org/protobuf/encoding/protowire" +) + +func TestMFAMetadataRoundTripAndTampering(t *testing.T) { + key := NewSVRKey([32]byte(bytes.Repeat([]byte{42}, 32))) + for _, name := range []string{"", "laptop", strings.Repeat("x", MFAKeyNameMaxLen), strings.Repeat("\U0001f511", 24) + "xy"} { + for _, millis := range []uint64{0, 999, 1000, 1721234567890, math.MaxUint64} { + metadata, err := NewMFAMetadata(name, millis) + if err != nil { + t.Fatal(err) + } + iv := bytes.Repeat([]byte{7}, aes.BlockSize) + encrypted, err := metadata.Encrypt(key, bytes.NewReader(iv)) + if err != nil { + t.Fatal(err) + } + raw := encrypted.Bytes() + if len(raw) != 160 || !bytes.Equal(raw[:aes.BlockSize], iv) { + t.Fatal("MFA ciphertext changed fixed size or supplied IV") + } + parsed, err := ParseEncryptedMFAMetadata(raw[:]) + if err != nil { + t.Fatal(err) + } + raw[0] ^= 1 + if parsed != encrypted { + t.Fatal("parsed ciphertext aliases caller memory") + } + decoded, err := parsed.Decrypt(key) + if err != nil || decoded.Name() != name || decoded.CreatedAtEpochMillis() != millis/1000*1000 { + t.Fatalf("MFA round trip failed: %v", err) + } + for i := range encrypted { + mutated := encrypted + mutated[i] ^= 1 + got, err := mutated.Decrypt(key) + if err == nil || got != (MFAMetadata{}) { + t.Fatalf("tampered byte %d returned metadata or no error", i) + } + } + if got, err := encrypted.Decrypt(NewSVRKey([32]byte{})); err == nil || got != (MFAMetadata{}) { + t.Fatal("wrong SVR key returned metadata or no error") + } + } + } +} + +func TestMFAMetadataRejectsInvalidNamesAndRandomness(t *testing.T) { + for _, name := range []string{"\xff", "contains\x00nul", strings.Repeat("x", 99), strings.Repeat("\U0001f511", 25)} { + got, err := NewMFAMetadata(name, 0) + if err == nil || got != (MFAMetadata{}) || strings.Contains(err.Error(), name) { + t.Fatal("invalid MFA name was accepted, returned metadata, or leaked into its error") + } + } + metadata, err := NewMFAMetadata("key", 0) + if err != nil { + t.Fatal(err) + } + for _, reader := range []io.Reader{nil, bytes.NewReader(nil), bytes.NewReader(make([]byte, 15))} { + got, err := metadata.Encrypt(NewSVRKey([32]byte{}), reader) + if err == nil || got != (EncryptedMFAMetadata{}) { + t.Fatal("failed randomness returned a partial ciphertext or no error") + } + } + for _, length := range []int{0, 159, 161, 320} { + got, err := ParseEncryptedMFAMetadata(make([]byte, length)) + if err == nil || got != (EncryptedMFAMetadata{}) { + t.Fatalf("accepted invalid ciphertext length %d", length) + } + } +} + +// Seal hostile, authenticated plaintext independently of the production encoder. +func sealMFATestPlaintext(t *testing.T, padded []byte) EncryptedMFAMetadata { + t.Helper() + if len(padded) != 112 { + t.Fatal("test plaintext must be exactly 112 bytes") + } + cipherKey := mustHex32(t, "3ac203547d12b86aedc2c0ed76f4d80b2d504fa8859a85153378b2694af67116") + authKey := mustHex32(t, "8f89663ca56515705125d2208be3a5828147fa7326e0796edc26871dfe7c8aa0") + block, err := aes.NewCipher(cipherKey[:]) + if err != nil { + t.Fatal(err) + } + var out EncryptedMFAMetadata + cipher.NewCBCEncrypter(block, out[:16]).CryptBlocks(out[16:128], padded) + mac := hmac.New(sha256.New, authKey[:]) + _, _ = mac.Write(out[:128]) + copy(out[128:], mac.Sum(nil)) + return out +} + +func padMFATestPlaintext(t *testing.T, plaintext []byte) []byte { + t.Helper() + if len(plaintext) >= 112 { + t.Fatal("test plaintext leaves no room for padding") + } + padding := byte(112 - len(plaintext)) // #nosec G115 -- the test guard bounds padding to 1..112. + return append(bytes.Clone(plaintext), bytes.Repeat([]byte{padding}, 112-len(plaintext))...) +} + +func TestMFAMetadataRejectsAuthenticatedMalformedPlaintext(t *testing.T) { + nameProto := func(name string) []byte { + return protowire.AppendString(protowire.AppendTag(nil, 2, protowire.BytesType), name) + } + overflow := protowire.AppendVarint(protowire.AppendTag(nil, 1, protowire.VarintType), math.MaxUint64/1000+1) + badPadding := bytes.Repeat([]byte{2}, 112) + badPadding[110] = 1 + cases := map[string][]byte{ + "zero padding": make([]byte, 112), + "oversize padding": bytes.Repeat([]byte{113}, 112), + "unequal padding": badPadding, + "invalid protobuf": padMFATestPlaintext(t, []byte{0xff}), + "oversize tag": padMFATestPlaintext(t, []byte{0x88, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00}), + "oversize length": padMFATestPlaintext(t, []byte{0x12, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00}), + "group tag bounds": padMFATestPlaintext(t, []byte{0x1b, 0x88, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x1c}), + "unterminated group": padMFATestPlaintext(t, []byte{0x1b}), + "top-level end group": padMFATestPlaintext(t, []byte{0x1c}), + "invalid UTF-8": padMFATestPlaintext(t, nameProto("\xff")), + "NUL name": padMFATestPlaintext(t, nameProto("key\x00name")), + "oversize name": padMFATestPlaintext(t, nameProto(strings.Repeat("x", 99))), + "time overflow": padMFATestPlaintext(t, overflow), + } + key := NewSVRKey([32]byte(bytes.Repeat([]byte{42}, 32))) + for name, padded := range cases { + t.Run(name, func(t *testing.T) { + got, err := sealMFATestPlaintext(t, padded).Decrypt(key) + if err == nil || got != (MFAMetadata{}) { + t.Fatal("authenticated malformed plaintext returned metadata or no error") + } + }) + } + // Upstream accepts unknown protobuf fields for forward compatibility. + unknown := protowire.AppendVarint(protowire.AppendTag(nameProto("key"), 3, protowire.VarintType), 42) + got, err := sealMFATestPlaintext(t, padMFATestPlaintext(t, unknown)).Decrypt(key) + if err != nil || got.Name() != "key" { + t.Fatalf("unknown protobuf field rejected: %v", err) + } + for _, plaintext := range [][]byte{ + {0x88, 0x80, 0x80, 0x80, 0x00, 0x00}, // Non-minimal, five-byte tag. + {0x12, 0x80, 0x80, 0x80, 0x80, 0x00}, // Non-minimal, five-byte length. + {0x1b, 0x24}, // Upstream skips groups even with mismatched end-field numbers. + {0x1b, 0x12, 0x01, 0xff, 0x1c}, // Known fields inside unknown groups are not interpreted. + } { + got, err := sealMFATestPlaintext(t, padMFATestPlaintext(t, plaintext)).Decrypt(key) + if err != nil || got != (MFAMetadata{}) { + t.Fatalf("upstream-compatible non-minimal/group encoding rejected: %v", err) + } + } +} + +func FuzzMFAMetadataRoundTrip(f *testing.F) { + f.Add("key", uint64(1721234567890), bytes.Repeat([]byte{42}, 32)) + f.Add("", uint64(0), make([]byte, 32)) + f.Fuzz(func(t *testing.T, name string, millis uint64, raw []byte) { + if len(raw) != 32 { + return + } + metadata, err := NewMFAMetadata(name, millis) + if err != nil { + return + } + key := NewSVRKey([32]byte(raw)) + encrypted, err := metadata.Encrypt(key, bytes.NewReader(make([]byte, 16))) + if err != nil { + t.Fatal(err) + } + got, err := encrypted.Decrypt(key) + if err != nil || got.Name() != name || got.CreatedAtEpochMillis() != millis/1000*1000 { + t.Fatal("MFA round trip failed") + } + }) +} + +func FuzzMFAMetadataDecrypt(f *testing.F) { + f.Add(make([]byte, 160)) + f.Fuzz(func(t *testing.T, raw []byte) { + encrypted, err := ParseEncryptedMFAMetadata(raw) + if err != nil { + return + } + got, err := encrypted.Decrypt(NewSVRKey([32]byte{})) + if err != nil && got != (MFAMetadata{}) { + t.Fatal("failed decryption returned metadata") + } + }) +} + +func FuzzMFAMetadataAuthenticatedPlaintext(f *testing.F) { + f.Add([]byte{}) + f.Add([]byte{0x12, 0x03, 'k', 'e', 'y'}) + f.Add([]byte{0x88, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00}) + f.Fuzz(func(t *testing.T, raw []byte) { + if len(raw) > 112 { + return + } + padded := raw + if len(raw) < 112 { + padded = padMFATestPlaintext(t, raw) + } + key := NewSVRKey([32]byte(bytes.Repeat([]byte{42}, 32))) + metadata, err := sealMFATestPlaintext(t, padded).Decrypt(key) + if err != nil { + if metadata != (MFAMetadata{}) { + t.Fatal("malformed authenticated plaintext returned metadata") + } + return + } + if _, err := NewMFAMetadata(metadata.Name(), metadata.CreatedAtEpochMillis()); err != nil || metadata.CreatedAtEpochMillis()%1000 != 0 { + t.Fatal("authenticated parser returned invalid metadata") + } + encrypted, err := metadata.Encrypt(key, bytes.NewReader(make([]byte, 16))) + if err != nil { + t.Fatal(err) + } + decoded, err := encrypted.Decrypt(key) + if err != nil || decoded != metadata { + t.Fatal("authenticated parsed metadata failed canonical round trip") + } + }) +} diff --git a/accountkeys/public_api_test.go b/accountkeys/public_api_test.go new file mode 100644 index 000000000..b54da6d85 --- /dev/null +++ b/accountkeys/public_api_test.go @@ -0,0 +1,73 @@ +package accountkeys_test + +import ( + "bytes" + "fmt" + "io" + "reflect" + "testing" + + "github.com/GoCodeAlone/libsignal-go/accountkeys" +) + +// Compile the additive public contract as a downstream consumer, without internals. +var ( + _ func([32]byte) accountkeys.SVRKey = accountkeys.NewSVRKey + _ func(string, uint64) (accountkeys.MFAMetadata, error) = accountkeys.NewMFAMetadata + _ func([]byte) (accountkeys.EncryptedMFAMetadata, error) = accountkeys.ParseEncryptedMFAMetadata + _ interface { + fmt.Stringer + fmt.GoStringer + fmt.Formatter + DeriveRegistrationLock() [32]byte + DeriveRegistrationRecoveryPassword() [32]byte + DeriveStorageServiceKey() [32]byte + DeriveLoggingKey() [32]byte + } = accountkeys.SVRKey{} + _ interface { + SVRKey() accountkeys.SVRKey + DeriveSVRKey() [32]byte + } = accountkeys.AccountEntropyPool{} + _ interface { + EncodeMasterKey([32]byte) [48]byte + DecodeMasterKey([48]byte) ([32]byte, error) + } = accountkeys.PinHash{} + _ interface { + Name() string + CreatedAtEpochMillis() uint64 + Encrypt(accountkeys.SVRKey, io.Reader) (accountkeys.EncryptedMFAMetadata, error) + } = accountkeys.MFAMetadata{} + _ interface { + Bytes() [160]byte + Decrypt(accountkeys.SVRKey) (accountkeys.MFAMetadata, error) + } = accountkeys.EncryptedMFAMetadata{} +) + +func TestPublicAccountKeyAPI(t *testing.T) { + keyType := reflect.TypeFor[accountkeys.SVRKey]() + for i := range keyType.NumField() { + field := keyType.Field(i) + if field.IsExported() { + t.Fatal("SVR master key has an exported raw field") + } + } + if keyType.NumMethod() != 7 { + t.Fatalf("SVR key has %d methods, want only the approved seven", keyType.NumMethod()) + } + metadata, err := accountkeys.NewMFAMetadata("security key", 1234567) + if err != nil { + t.Fatal(err) + } + key := accountkeys.NewSVRKey([32]byte{42}) + encrypted, err := metadata.Encrypt(key, bytes.NewReader(make([]byte, 16))) + if err != nil { + t.Fatal(err) + } + decrypted, err := encrypted.Decrypt(key) + if err != nil || decrypted.Name() != metadata.Name() || decrypted.CreatedAtEpochMillis() != 1234000 { + t.Fatalf("downstream consumer round trip failed: %v", err) + } + if accountkeys.MediaEncryptionAESKeyLen+accountkeys.MediaEncryptionHMACKeyLen != accountkeys.MediaEncryptionKeyLen { + t.Fatal("media encryption key split changed") + } +} diff --git a/accountkeys/svr_key.go b/accountkeys/svr_key.go new file mode 100644 index 000000000..8ff59b558 --- /dev/null +++ b/accountkeys/svr_key.go @@ -0,0 +1,97 @@ +package accountkeys + +import ( + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "errors" + "fmt" +) + +// SVRKey is the account's private Secure Value Recovery master key. +type SVRKey struct { + bytes [SVRKeyLen]byte +} + +// NewSVRKey wraps an existing Secure Value Recovery master key. +func NewSVRKey(raw [SVRKeyLen]byte) SVRKey { return SVRKey{bytes: raw} } + +// SVRKey derives a typed Secure Value Recovery master key from account entropy. +func (p AccountEntropyPool) SVRKey() SVRKey { return NewSVRKey(p.DeriveSVRKey()) } + +// String redacts the key rather than exposing secret bytes. +func (k SVRKey) String() string { return "[REDACTED SVRKey]" } + +// GoString redacts the key in Go-syntax formatting. +func (k SVRKey) GoString() string { return k.String() } + +// Format redacts the key for every format verb, flag, width and precision. +func (k SVRKey) Format(state fmt.State, _ rune) { _, _ = state.Write([]byte(k.String())) } + +// DeriveRegistrationLock derives the registration-lock token. +func (k SVRKey) DeriveRegistrationLock() [32]byte { + return keyedHash(k.bytes[:], []byte("Registration Lock")) +} + +// DeriveRegistrationRecoveryPassword derives the account recovery password. +func (k SVRKey) DeriveRegistrationRecoveryPassword() [32]byte { + return keyedHash(k.bytes[:], []byte("Registration Recovery")) +} + +// DeriveStorageServiceKey derives the storage-service root encryption key. +func (k SVRKey) DeriveStorageServiceKey() [32]byte { + return keyedHash(k.bytes[:], []byte("Storage Service Encryption")) +} + +// DeriveLoggingKey derives the logging pseudonym key. +func (k SVRKey) DeriveLoggingKey() [32]byte { + return keyedHash(k.bytes[:], []byte("Logging Key")) +} + +// EncodeMasterKey authenticates and encrypts masterKey using upstream's PIN SIV format. +func (h PinHash) EncodeMasterKey(masterKey [32]byte) [48]byte { + defer clear(masterKey[:]) + authKey := keyedHash(h.EncryptionKey[:], []byte("auth")) + defer clear(authKey[:]) + encKey := keyedHash(h.EncryptionKey[:], []byte("enc")) + defer clear(encKey[:]) + iv := keyedHash(authKey[:], masterKey[:]) + defer clear(iv[:]) + mask := keyedHash(encKey[:], iv[:16]) + defer clear(mask[:]) + var out [48]byte + copy(out[:16], iv[:16]) + for i := range masterKey { + out[16+i] = masterKey[i] ^ mask[i] + } + return out +} + +// DecodeMasterKey authenticates and decrypts an upstream PIN SIV master key. +func (h PinHash) DecodeMasterKey(ciphertext [48]byte) ([32]byte, error) { + authKey := keyedHash(h.EncryptionKey[:], []byte("auth")) + defer clear(authKey[:]) + encKey := keyedHash(h.EncryptionKey[:], []byte("enc")) + defer clear(encKey[:]) + mask := keyedHash(encKey[:], ciphertext[:16]) + defer clear(mask[:]) + var masterKey [32]byte + for i := range masterKey { + masterKey[i] = ciphertext[16+i] ^ mask[i] + } + iv := keyedHash(authKey[:], masterKey[:]) + defer clear(iv[:]) + if subtle.ConstantTimeCompare(iv[:16], ciphertext[:16]) != 1 { + clear(masterKey[:]) + return [32]byte{}, errors.New("invalid PIN master-key authentication") + } + return masterKey, nil +} + +func keyedHash(key, input []byte) [32]byte { + mac := hmac.New(sha256.New, key) + _, _ = mac.Write(input) + var out [32]byte + mac.Sum(out[:0]) + return out +} diff --git a/accountkeys/svr_key_test.go b/accountkeys/svr_key_test.go new file mode 100644 index 000000000..447965be7 --- /dev/null +++ b/accountkeys/svr_key_test.go @@ -0,0 +1,108 @@ +package accountkeys + +import ( + "bytes" + "encoding/hex" + "fmt" + "log" + "log/slog" + "strings" + "testing" +) + +func TestPinHashMasterKeySIV(t *testing.T) { + type masterKeyCodec interface { + EncodeMasterKey([32]byte) [48]byte + DecodeMasterKey([48]byte) ([32]byte, error) + } + hash := CreatePinHash([]byte("password"), mustHex32(t, "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) + codec, ok := any(hash).(masterKeyCodec) + if !ok { + t.Fatal("PinHash is missing upstream master-key SIV encoding and decoding") + } + master := mustHex32(t, "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f") + ciphertext := codec.EncodeMasterKey(master) + const want = "3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769" + if hex.EncodeToString(ciphertext[:]) != want { + t.Fatalf("master-key SIV = %x, want %s", ciphertext, want) + } + decoded, err := codec.DecodeMasterKey(ciphertext) + if err != nil || decoded != master { + t.Fatalf("master-key round trip failed: %v", err) + } + for i := range ciphertext { + mutated := ciphertext + mutated[i] ^= 1 + got, err := codec.DecodeMasterKey(mutated) + if err == nil || got != ([32]byte{}) { + t.Fatalf("tampered byte %d returned plaintext or no error", i) + } + } + wrong := hash + wrong.EncryptionKey[0] ^= 1 + if got, err := any(wrong).(masterKeyCodec).DecodeMasterKey(ciphertext); err == nil || got != ([32]byte{}) { + t.Fatal("wrong PIN encryption key returned plaintext or no error") + } +} + +func TestSVRDerivationsAndRedaction(t *testing.T) { + key := NewSVRKey([32]byte(bytes.Repeat([]byte{42}, 32))) + cases := []struct { + name string + got [32]byte + want string + }{ + {"registration lock", key.DeriveRegistrationLock(), "3a40e25812e6c20cca76a602451dd2bc7484553514438cade320c2aef54e10d1"}, + {"recovery password", key.DeriveRegistrationRecoveryPassword(), "91f959cfee39676dedd028bc8bbbd1e91ffa6a42c57754d095fe8abe7f0d4f56"}, + {"storage service", key.DeriveStorageServiceKey(), "3f31b618172a9f8ad45e290788e6176736e6161d4ea0e8050f8553521f59c200"}, + {"logging", key.DeriveLoggingKey(), "cd2a39f4857de4df3fe793d1de061bfa3dd63533c0a4ef79b3fa3eba2bf96e62"}, + } + for _, tc := range cases { + if tc.got != mustHex32(t, tc.want) { + t.Errorf("%s derivation = %x, want %s", tc.name, tc.got, tc.want) + } + } + pool, err := ParseAccountEntropyPool(strings.Repeat("m", 64)) + if err != nil { + t.Fatal(err) + } + if got, want := pool.SVRKey().DeriveRegistrationLock(), NewSVRKey(pool.DeriveSVRKey()).DeriveRegistrationLock(); got != want { + t.Fatal("typed SVR key changed legacy entropy derivation") + } + for _, format := range []string{"%v", "%+v", "%#v", "%x", "%s", "%q", "%100x", "%.4s"} { + for _, value := range []any{key, &key} { + if got := fmt.Sprintf(format, value); got != "[REDACTED SVRKey]" { + t.Errorf("format %q = %q, want unconditional redaction", format, got) + } + } + } + var out bytes.Buffer + log.New(&out, "", 0).Print(key) + slog.New(slog.NewTextHandler(&out, nil)).Info("key", "svr", key) + fmt.Fprint(&out, fmt.Errorf("operation failed: %w", fmt.Errorf("key=%#v", key))) + if strings.Contains(out.String(), strings.Repeat("2a", 32)) || strings.Contains(out.String(), "42 42") { + t.Fatal("logging or wrapped errors leaked SVR key bytes") + } + if strings.Count(out.String(), "REDACTED SVRKey") != 3 { + t.Fatalf("expected redaction in log, slog and wrapped errors: %s", out.String()) + } +} + +func FuzzPinHashMasterKeySIV(f *testing.F) { + f.Add(bytes.Repeat([]byte{42}, 32), bytes.Repeat([]byte{1}, 32)) + f.Fuzz(func(t *testing.T, key, master []byte) { + if len(key) != 32 || len(master) != 32 { + return + } + hash := PinHash{EncryptionKey: [32]byte(key)} + ciphertext := hash.EncodeMasterKey([32]byte(master)) + decoded, err := hash.DecodeMasterKey(ciphertext) + if err != nil || decoded != [32]byte(master) { + t.Fatal("SIV round trip failed") + } + ciphertext[0] ^= 1 + if got, err := hash.DecodeMasterKey(ciphertext); err == nil || got != ([32]byte{}) { + t.Fatal("unauthenticated SIV returned plaintext") + } + }) +} diff --git a/compat/account_keys_v01040_test.go b/compat/account_keys_v01040_test.go new file mode 100644 index 000000000..7a0d6dd6c --- /dev/null +++ b/compat/account_keys_v01040_test.go @@ -0,0 +1,188 @@ +package compat + +import ( + "bytes" + "testing" + + "github.com/GoCodeAlone/libsignal-go/accountkeys" +) + +func accountKey32(t *testing.T, encoded string) [32]byte { + t.Helper() + raw := mustHex(t, encoded) + if len(raw) != 32 { + t.Fatalf("account-key fixture: expected 32 bytes, got %d", len(raw)) + } + return [32]byte(raw) +} + +func TestAccountKeysV01040Vectors(t *testing.T) { + var batch struct { + Note struct { + UpstreamTag string `json:"upstream_tag"` + UpstreamCommit string `json:"upstream_commit"` + UpstreamTagObject string `json:"upstream_tag_object"` + } `json:"_note"` + SVRCases []struct { + Label string `json:"label"` + SVRKey string `json:"svr_key"` + RegistrationLock string `json:"registration_lock"` + RegistrationRecoveryPassword string `json:"registration_recovery_password"` + StorageServiceKey string `json:"storage_service_key"` + LoggingKey string `json:"logging_key"` + } `json:"svr_cases"` + PinCases []struct { + PIN string `json:"pin"` + Salt string `json:"salt"` + EncryptionKey string `json:"encryption_key"` + MasterKey string `json:"master_key"` + EncodedMasterKey string `json:"encrypted_master_key"` + DecodedMasterKey string `json:"decoded_master_key"` + Tampered []struct { + EncodedMasterKey string `json:"encrypted_master_key"` + Accepted bool `json:"accepted"` + } `json:"tampered"` + } `json:"pin_cases"` + Cases []struct { + EntropyPool string `json:"entropy_pool"` + MediaName string `json:"media_name"` + MediaID string `json:"media_id"` + MediaKey string `json:"media_encryption_key_data"` + MediaAESKey string `json:"media_aes_key"` + MediaHMACKey string `json:"media_hmac_key"` + ThumbnailKey string `json:"thumbnail_transit_encryption_key_data"` + ThumbnailAES string `json:"thumbnail_aes_key"` + ThumbnailHMAC string `json:"thumbnail_hmac_key"` + } `json:"cases"` + MFACases []struct { + Label string `json:"label"` + SVRKey string `json:"svr_key"` + Name string `json:"name"` + CreatedAtMillis uint64 `json:"created_at_millis"` + IV string `json:"iv"` + EncryptedMetadata string `json:"encrypted_metadata"` + DecryptedName string `json:"decrypted_name"` + DecryptedCreatedAtMillis uint64 `json:"decrypted_created_at_millis"` + } `json:"mfa_cases"` + MFAInvalidCases []struct { + Label string `json:"label"` + SVRKey string `json:"svr_key"` + EncryptedMetadata string `json:"encrypted_metadata"` + Accepted bool `json:"accepted"` + } `json:"mfa_invalid_cases"` + MFAInvalidNames []struct { + Name string `json:"name"` + CreatedAtMillis uint64 `json:"created_at_millis"` + } `json:"mfa_invalid_name_cases"` + } + loadVectors(t, "account-keys", &batch) + if batch.Note.UpstreamTag != "v0.104.0" || batch.Note.UpstreamCommit != "257105c55a7389ca6b1e85185e2769465e6729f1" || batch.Note.UpstreamTagObject != "03b9987415e6dcb3d83dfde6a16b52d2b59a7b44" { + t.Fatal("account-key oracle fixture lacks the approved immutable upstream pin") + } + if len(batch.SVRCases) < 4 || len(batch.PinCases) < 2 || len(batch.Cases) == 0 || len(batch.MFACases) < 5 || len(batch.MFAInvalidCases) < 6 || len(batch.MFAInvalidNames) < 3 { + t.Fatal("account-key oracle fixture is missing required coverage") + } + for _, tc := range batch.SVRCases { + t.Run("svr/"+tc.Label, func(t *testing.T) { + key := accountkeys.NewSVRKey(accountKey32(t, tc.SVRKey)) + for _, check := range []struct { + got [32]byte + want string + }{ + {key.DeriveRegistrationLock(), tc.RegistrationLock}, + {key.DeriveRegistrationRecoveryPassword(), tc.RegistrationRecoveryPassword}, + {key.DeriveStorageServiceKey(), tc.StorageServiceKey}, + {key.DeriveLoggingKey(), tc.LoggingKey}, + } { + if check.got != accountKey32(t, check.want) { + t.Fatal("SVR derivation differs from upstream") + } + } + }) + } + for _, tc := range batch.PinCases { + hash := accountkeys.CreatePinHash([]byte(tc.PIN), accountKey32(t, tc.Salt)) + if hash.EncryptionKey != accountKey32(t, tc.EncryptionKey) { + t.Fatal("PIN encryption key differs from upstream") + } + encoded := hash.EncodeMasterKey(accountKey32(t, tc.MasterKey)) + if !bytes.Equal(encoded[:], mustHex(t, tc.EncodedMasterKey)) { + t.Fatal("PIN master-key encryption differs from upstream") + } + decoded, err := hash.DecodeMasterKey(encoded) + if err != nil || decoded != accountKey32(t, tc.DecodedMasterKey) { + t.Fatal("PIN master-key decryption differs from upstream") + } + if len(tc.Tampered) < 3 { + t.Fatal("PIN fixture lacks tampered IV and ciphertext cases") + } + for _, invalid := range tc.Tampered { + got, err := hash.DecodeMasterKey([48]byte(mustHex(t, invalid.EncodedMasterKey))) + if invalid.Accepted || err == nil || got != ([32]byte{}) { + t.Fatal("PIN tamper rejection differs from upstream") + } + } + } + for _, tc := range batch.Cases { + pool, err := accountkeys.ParseAccountEntropyPool(tc.EntropyPool) + if err != nil { + t.Fatal(err) + } + backup := accountkeys.DeriveBackupKey(pool) + id := backup.DeriveMediaID(tc.MediaName) + if !bytes.Equal(id[:], mustHex(t, tc.MediaID)) { + t.Fatal("media ID differs from upstream") + } + for _, check := range []struct { + got [accountkeys.MediaEncryptionKeyLen]byte + full, aes string + hmac string + }{ + {backup.DeriveMediaEncryptionKeyData(id), tc.MediaKey, tc.MediaAESKey, tc.MediaHMACKey}, + {backup.DeriveThumbnailTransitEncryptionKeyData(id), tc.ThumbnailKey, tc.ThumbnailAES, tc.ThumbnailHMAC}, + } { + if !bytes.Equal(check.got[:], mustHex(t, check.full)) || !bytes.Equal(check.got[:accountkeys.MediaEncryptionAESKeyLen], mustHex(t, check.aes)) || !bytes.Equal(check.got[accountkeys.MediaEncryptionKeyLen-accountkeys.MediaEncryptionHMACKeyLen:], mustHex(t, check.hmac)) { + t.Fatal("media key data or AES/HMAC split differs from upstream") + } + } + } + for _, tc := range batch.MFACases { + t.Run("mfa/"+tc.Label, func(t *testing.T) { + key := accountkeys.NewSVRKey(accountKey32(t, tc.SVRKey)) + metadata, err := accountkeys.NewMFAMetadata(tc.Name, tc.CreatedAtMillis) + if err != nil { + t.Fatal(err) + } + encrypted, err := metadata.Encrypt(key, bytes.NewReader(mustHex(t, tc.IV))) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(encrypted[:], mustHex(t, tc.EncryptedMetadata)) { + t.Fatal("Go MFA ciphertext differs from real upstream Rust output") + } + parsed, err := accountkeys.ParseEncryptedMFAMetadata(mustHex(t, tc.EncryptedMetadata)) + if err != nil { + t.Fatal(err) + } + decrypted, err := parsed.Decrypt(key) + if err != nil || decrypted.Name() != tc.DecryptedName || decrypted.CreatedAtEpochMillis() != tc.DecryptedCreatedAtMillis { + t.Fatal("Go decryption of upstream MFA ciphertext differs from upstream") + } + }) + } + for _, tc := range batch.MFAInvalidCases { + encrypted, err := accountkeys.ParseEncryptedMFAMetadata(mustHex(t, tc.EncryptedMetadata)) + var decrypted accountkeys.MFAMetadata + if err == nil { + decrypted, err = encrypted.Decrypt(accountkeys.NewSVRKey(accountKey32(t, tc.SVRKey))) + } + if tc.Accepted || err == nil || decrypted != (accountkeys.MFAMetadata{}) { + t.Fatalf("upstream rejected MFA case %q but Go returned plaintext or no error", tc.Label) + } + } + for _, tc := range batch.MFAInvalidNames { + if got, err := accountkeys.NewMFAMetadata(tc.Name, tc.CreatedAtMillis); err == nil || got != (accountkeys.MFAMetadata{}) { + t.Fatal("Go accepted an MFA name rejected by upstream") + } + } +} diff --git a/compat/coverage_manifest.json b/compat/coverage_manifest.json index 9df578d49..f7198660c 100644 --- a/compat/coverage_manifest.json +++ b/compat/coverage_manifest.json @@ -6,14 +6,14 @@ "status": "vector-backed", "vector": "vectors/account-keys.json", "packages": ["accountkeys"], - "notes": "Account entropy pool parsing, SVR key derivation, backup key derivation, backup ID derivation, and PIN access key vectors." + "notes": "Legacy v0.96.4 entropy/backup/PIN vectors plus v0.104.0 SVR derivations, PIN master-key SIV, media key splits, and fixed-size MFA metadata. The fixture _note records the new upstream commit separately from the global baseline." }, { "name": "svr-key", "status": "vector-backed", "vector": "vectors/account-keys.json", "packages": ["accountkeys"], - "notes": "SVR key derivation is covered as part of the account-keys vector batch." + "notes": "Entropy-to-SVR derivation plus v0.104.0 registration lock, recovery password, storage service, and logging-key vectors. SVRKey has private bytes and redacted formatting, with no new raw-key accessor." }, { "name": "backup-id", @@ -27,7 +27,7 @@ "status": "vector-backed", "vector": "vectors/account-keys.json", "packages": ["accountkeys"], - "notes": "Backup key, backup ID, local metadata key, media key, thumbnail key, and backup forward-secrecy derivation formulas are owned by accountkeys; committed fixtures currently vector-lock the root backup key and backup ID." + "notes": "Backup derivation formulas are owned by accountkeys. Committed fixtures vector-lock root backup key and backup ID at v0.96.4, plus media/thumbnail key data and AES/HMAC splits at v0.104.0. Local metadata and forward-secrecy derivations are covered by Go tests, not new upstream vectors." }, { "name": "username-links", diff --git a/compat/rust-harness/Cargo.lock b/compat/rust-harness/Cargo.lock index 6db3d4649..7009ae66c 100644 --- a/compat/rust-harness/Cargo.lock +++ b/compat/rust-harness/Cargo.lock @@ -4,38 +4,38 @@ version = 4 [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common", - "generic-array", + "crypto-common 0.2.2", + "inout", ] [[package]] name = "aes" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cfg-if", "cipher", - "cpufeatures", + "cpubits", + "cpufeatures 0.3.1", "zeroize", ] [[package]] name = "aes-gcm-siv" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +checksum = "4c32325128cb24de2a2d14d92da7569dae7073f9e47334521d494747fa94d902" dependencies = [ "aead", "aes", "cipher", "ctr", + "ctutils", "polyval", - "subtle", "zeroize", ] @@ -54,6 +54,19 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", + "zeroize", +] + [[package]] name = "assert_matches" version = "1.5.0" @@ -77,12 +90,47 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn", +] + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -92,13 +140,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + [[package]] name = "block-padding" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -115,9 +173,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cbc" -version = "0.1.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ "cipher", ] @@ -129,7 +187,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", ] [[package]] @@ -138,17 +205,52 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common", + "block-buffer 0.12.1", + "crypto-common 0.2.2", "inout", "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-str" version = "1.1.0" @@ -157,15 +259,21 @@ checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] name = "core-models" -version = "0.0.5" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4" +checksum = "ee3344d349528f449816cfa85e558c439bdca5a6d8a738cfb21e69f0b2b1952f" dependencies = [ "hax-lib", "pastey", - "rand", + "rand 0.10.3", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -175,6 +283,26 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crabgrind" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7459e07732f6de74001fcf73a3fdfbb0f368e4fd8e2392f4a2206a065e6e95" +dependencies = [ + "bindgen", + "cc", + "pkg-config", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -207,31 +335,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.2", + "hybrid-array", + "rand_core 0.10.1", +] + [[package]] name = "ctr" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", "curve25519-dalek-derive", - "digest", + "digest 0.11.3", "fiat-crypto", - "rand_core 0.6.4", "rustc_version", "subtle", "zeroize", @@ -312,11 +458,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -347,7 +505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -358,9 +516,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" @@ -510,21 +668,27 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] [[package]] name = "ghash" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "opaque-debug", "polyval", "zeroize", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "hashbrown" version = "0.15.5" @@ -542,9 +706,9 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hax-lib" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +checksum = "01a33cb227f97ee5c419064c31d7c55a7d895f28d8019ccdadc311cc036500c4" dependencies = [ "hax-lib-macros", "num-bigint", @@ -553,9 +717,9 @@ dependencies = [ [[package]] name = "hax-lib-macros" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +checksum = "28da835a5f153e9f3d0cc1f42ee605a1cfa9093c93348193793d60a1b0908b51" dependencies = [ "hax-lib-macros-types", "proc-macro-error2", @@ -566,9 +730,9 @@ dependencies = [ [[package]] name = "hax-lib-macros-types" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +checksum = "eed327e9a28d6ee018a4a9ba842d4ee27e378c8a591d2beb6f42f32b24a02fcb" dependencies = [ "proc-macro2", "quote", @@ -591,43 +755,61 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ "hmac", ] [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] name = "hpke-rs" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ad6a58eb3e0ee30be8bfc7a9770ae98adcfa1d9bc820a5847732ce84f70837" +checksum = "812de62ab573b876c6c40d7553bae9402ae3bad95b64af06f964388eecb9b7b1" dependencies = [ "hpke-rs-crypto", "libcrux-sha3", "log", - "rand_core 0.9.5", "subtle", "zeroize", ] [[package]] name = "hpke-rs-crypto" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a73a99d9008010d73289f41335a3f6e14fb8c04eaf60e9111b450463b1bbc7f" +checksum = "d2c461da2e0c9c93d875b597f0c78214fb0d2d5c57834b2ef2a2fa057fa20e24" dependencies = [ - "rand_core 0.9.5", + "rand 0.10.3", + "zeroize", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", "zeroize", ] @@ -651,12 +833,21 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ "block-padding", - "generic-array", + "hybrid-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", ] [[package]] @@ -668,6 +859,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -699,29 +899,30 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libcrux-hacl-rs" -version = "0.0.4" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2637dc87d158e1f1b550fd9b226443e84153fded4de69028d897b534d16d22e6" +checksum = "66106db376ae249af86911aee048cf73ea1f394d6653026fc988dd22cf2a4ace" dependencies = [ "libcrux-macros", ] [[package]] name = "libcrux-hmac" -version = "0.0.6" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7a242707d65960770bd7e14e4f18a92bdf0b967777dd404887db8d087a643b" +checksum = "500ad9b32c715161b594172ca1fb11503a1c033b393ff4c0c33505ce51c1943c" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", "libcrux-sha2", + "libcrux-traits", ] [[package]] name = "libcrux-intrinsics" -version = "0.0.6" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b5db005ff8001e026b73a6842ee81bbef8ec5ff0e1915a67ae65fd2a9fafa5" +checksum = "98a0c574d4eb81d0814bc2b91e4a433d7e0b35851b1d62eb5dd95c064f1f76d0" dependencies = [ "core-models", "hax-lib", @@ -739,9 +940,9 @@ dependencies = [ [[package]] name = "libcrux-ml-kem" -version = "0.0.8" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14ab3e477de9df6ee1273a114018ff62c4996ca9220070c4e5cb1743f94a67d" +checksum = "1d8160f7d64fd2716b4fd05cc886a042f8dcda18d9206c0d506e2c67bdf97daa" dependencies = [ "hax-lib", "libcrux-intrinsics", @@ -762,18 +963,19 @@ dependencies = [ [[package]] name = "libcrux-secrets" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +checksum = "79054fc9037cb70d6a546cf094cea7d7df06af5e49d230ba24103d27ccc886f1" dependencies = [ + "crabgrind", "hax-lib", ] [[package]] name = "libcrux-sha2" -version = "0.0.6" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d253473f259fc74a280c43f29c464f7e374abdf28b4942234dc707f529d4b7" +checksum = "960e46e1be0b77098cc6ce82137864936ecad3a1b9fd4303dd51202ca140dd1e" dependencies = [ "libcrux-hacl-rs", "libcrux-macros", @@ -782,9 +984,9 @@ dependencies = [ [[package]] name = "libcrux-sha3" -version = "0.0.8" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ae0b7d0e1cc4793a609fd0ff2ca3b3a3fabae523770c619a3d4bc86417b0d7" +checksum = "c09f5c39afae0528e1f70a3c1e3c6ee649ec1f19dda26fc9b6ea91108fb879ef" dependencies = [ "hax-lib", "libcrux-intrinsics", @@ -794,28 +996,65 @@ dependencies = [ [[package]] name = "libcrux-traits" -version = "0.0.6" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e4fa89f3f5e34b47f928b22b1b78395a0d4ec23b1f583db635f128159d65f" +checksum = "3fa7a21e8c2e8baa8b40f0b176740f2ca4baadd79d1b00e48d6b1e363c42085a" dependencies = [ "libcrux-secrets", - "rand", + "rand 0.10.3", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libsignal-account-keys" +version = "0.1.0" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" +dependencies = [ + "aes", + "argon2", + "cbc", + "derive_more", + "displaydoc", + "hkdf", + "hmac", + "libsignal-core", + "libsignal-protocol", + "partial-default", + "password-hash", + "protobuf", + "protobuf-codegen", + "rand 0.9.4", + "rand_core 0.9.5", + "sha2", + "static_assertions", + "subtle", + "thiserror 2.0.18", + "zerocopy", ] [[package]] name = "libsignal-core" version = "0.1.0" -source = "git+https://github.com/signalapp/libsignal?tag=v0.96.4#57d41c877d5a55d881fb3f52d9b16900118ee29b" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" dependencies = [ "curve25519-dalek", "derive_more", "displaydoc", "libsignal-debug", "log", - "rand", + "rand 0.9.4", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", "uuid", "x25519-dalek", "zerocopy", @@ -823,8 +1062,8 @@ dependencies = [ [[package]] name = "libsignal-debug" -version = "0.96.4" -source = "git+https://github.com/signalapp/libsignal?tag=v0.96.4#57d41c877d5a55d881fb3f52d9b16900118ee29b" +version = "0.104.0" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" dependencies = [ "cfg-if", ] @@ -832,7 +1071,7 @@ dependencies = [ [[package]] name = "libsignal-protocol" version = "0.1.0" -source = "git+https://github.com/signalapp/libsignal?tag=v0.96.4#57d41c877d5a55d881fb3f52d9b16900118ee29b" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" dependencies = [ "aes", "aes-gcm-siv", @@ -849,25 +1088,31 @@ dependencies = [ "hkdf", "hmac", "indexmap", - "itertools", + "itertools 0.15.0", "libcrux-ml-kem", "libsignal-core", "libsignal-debug", "log", "prost", "prost-build", - "rand", + "rand 0.9.4", "rayon", "serde", "sha2", "signal-crypto", "spqr", "subtle", - "thiserror", + "thiserror 2.0.18", "uuid", "zerocopy", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -886,12 +1131,28 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "multimap" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -949,10 +1210,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "opaque-debug" -version = "0.3.1" +name = "partial-default" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "124dc3c21ffb6fb3a0562d129929a8a54998766ef7adc1ba09ddc467d092c14b" +dependencies = [ + "partial-default-derive", +] + +[[package]] +name = "partial-default-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7459127d7a18cb202d418e4b7df1103ffd6d82a106e9b2091c250624c2ace70d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "password-hash" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] [[package]] name = "pastey" @@ -977,10 +1263,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "poksho" version = "0.7.0" -source = "git+https://github.com/signalapp/libsignal?tag=v0.96.4#57d41c877d5a55d881fb3f52d9b16900118ee29b" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" dependencies = [ "curve25519-dalek", "hmac", @@ -989,14 +1281,14 @@ dependencies = [ [[package]] name = "polyval" -version = "0.6.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", + "cpubits", + "cpufeatures 0.3.1", "universal-hash", + "zeroize", ] [[package]] @@ -1075,7 +1367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "petgraph", @@ -1094,7 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -1109,6 +1401,57 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "quote" version = "1.0.45" @@ -1140,6 +1483,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1168,6 +1522,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.12.0" @@ -1227,8 +1587,9 @@ dependencies = [ "hkdf", "hmac", "libcrux-ml-kem", + "libsignal-account-keys", "libsignal-protocol", - "rand", + "rand 0.9.4", "rand_chacha", "rand_core 0.9.5", "serde_json", @@ -1238,6 +1599,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1247,6 +1614,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1256,8 +1636,8 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", - "windows-sys", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -1317,35 +1697,31 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest", - "sha2-asm", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] -name = "sha2-asm" -version = "0.6.4" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" -dependencies = [ - "cc", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "shlex" @@ -1356,7 +1732,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-crypto" version = "0.1.0" -source = "git+https://github.com/signalapp/libsignal?tag=v0.96.4#57d41c877d5a55d881fb3f52d9b16900118ee29b" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" dependencies = [ "aes", "cbc", @@ -1375,7 +1751,7 @@ dependencies = [ "sha1", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", "zeroize", ] @@ -1393,12 +1769,10 @@ checksum = "19f58d7b0190c7f12df7e8be6b79767a0836059159811b869d5ab55721fe14d0" [[package]] name = "spqr" -version = "1.5.1" -source = "git+https://github.com/signalapp/SparsePostQuantumRatchet.git?tag=v1.5.1#f2589fef855c10f39d72634dab3d14654dd410bf" +version = "1.6.0" +source = "git+https://github.com/signalapp/SparsePostQuantumRatchet.git?tag=v1.6.0#06959b4708f9b7b1e94d0f8cc835f3958c077e94" dependencies = [ - "cpufeatures", - "curve25519-dalek", - "displaydoc", + "cpufeatures 0.3.1", "hax-lib", "hkdf", "libcrux-hmac", @@ -1407,13 +1781,19 @@ dependencies = [ "num_enum", "prost", "prost-build", - "rand", + "rand 0.9.4", "rand_core 0.9.5", "sha2", "sorted-vec", - "thiserror", + "thiserror 2.0.18", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.6.1" @@ -1438,9 +1818,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", + "getrandom 0.4.2", "once_cell", - "rustix", - "windows-sys", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -1449,7 +1839,18 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1513,18 +1914,18 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] name = "usernames" version = "0.1.0" -source = "git+https://github.com/signalapp/libsignal?tag=v0.96.4#57d41c877d5a55d881fb3f52d9b16900118ee29b" +source = "git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1#257105c55a7389ca6b1e85185e2769465e6729f1" dependencies = [ "curve25519-dalek", "displaydoc", @@ -1534,11 +1935,11 @@ dependencies = [ "poksho", "prost", "prost-build", - "rand", + "rand 0.9.4", "sha2", "signal-crypto", "subtle", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1661,12 +2062,33 @@ dependencies = [ "semver", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1676,6 +2098,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "1.0.3" @@ -1781,13 +2267,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", - "serde", + "rand_core 0.10.1", "zeroize", ] diff --git a/compat/rust-harness/Cargo.toml b/compat/rust-harness/Cargo.toml index 4ea607e23..1cd374d75 100644 --- a/compat/rust-harness/Cargo.toml +++ b/compat/rust-harness/Cargo.toml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # # Rust compatibility harness: wraps upstream libsignal-protocol pinned to a -# fixed tag, exposing deterministic test-vector generation and a JSON-RPC +# fixed commit, exposing deterministic test-vector generation and a JSON-RPC # interop loop. This is the behavioral reference oracle for the pure-Go port. # # The git dependency + isolated-workspace pattern mirrors upstream @@ -14,20 +14,22 @@ name = "rust-harness" version = "0.1.0" edition = "2021" +[package.metadata.libsignal] +upstream_tag = "v0.104.0" + # Prevent this crate from being pulled into any parent workspace. [workspace] members = ["."] [dependencies] -# Pinned to the current Stage-2 mainline-compat target. v0.96.4 is the latest -# upstream release reviewed on 2026-06-30. Its v0.96.0..v0.96.4 protocol-core -# delta is limited to inactive ML-KEM-1024 parameter metadata; the rest is -# net/client, bridge, and zkgroup API surface outside this module's protocol-core -# scope. The rand stack must match upstream's (rand 0.9 family) so a seeded +# The oracle is pinned to the peeled v0.104.0 release commit. The annotated +# tag object is 03b9987415e6dcb3d83dfde6a16b52d2b59a7b44. The rand stack must +# match upstream's (rand 0.9 family) so a seeded # ChaCha20Rng satisfies the rand_core 0.9 CryptoRng/RngCore bounds the libsignal # APIs take. -libsignal-protocol = { git = "https://github.com/signalapp/libsignal", tag = "v0.96.4", package = "libsignal-protocol" } -usernames = { git = "https://github.com/signalapp/libsignal", tag = "v0.96.4", package = "usernames" } +libsignal-protocol = { git = "https://github.com/signalapp/libsignal", rev = "257105c55a7389ca6b1e85185e2769465e6729f1", package = "libsignal-protocol" } +libsignal-account-keys = { git = "https://github.com/signalapp/libsignal", rev = "257105c55a7389ca6b1e85185e2769465e6729f1", package = "libsignal-account-keys" } +usernames = { git = "https://github.com/signalapp/libsignal", rev = "257105c55a7389ca6b1e85185e2769465e6729f1", package = "usernames" } rand = "0.9" rand_chacha = "0.9" rand_core = "0.9" @@ -37,12 +39,10 @@ rand_core = "0.9" futures-util = "0.3" # Crypto primitives for the hkdf domain: the chain/root/message-key and # pqxdh-secret derivations are pub(crate) upstream, so the harness reproduces -# them against the same crate versions, matching the v0.96.4 source exactly -# (these version-stable formulas are unchanged from v0.91.0 — the hkdf vectors -# are byte-identical across the re-pin). -hkdf = "0.12" -hmac = "0.12" -sha2 = "0.10" +# them against the same crate versions, matching the v0.104.0 source. +hkdf = "0.13" +hmac = "0.13" +sha2 = "0.11" serde_json = "1" hex = "0.4" uuid = "1" @@ -51,7 +51,7 @@ uuid = "1" # store) to completion in the interop RPC dispatch and the groups vectors. futures = "0.3" # The incremental ML-KEM-768 KEM used by SPQR (the Stage-2 PQ ratchet). Pinned to -# EXACTLY the version SPQR v1.5.1 pulls in transitively (see Cargo.lock), so the +# EXACTLY the version SPQR v1.6.0 pulls in transitively (see Cargo.lock), so the # graph has a single copy and the harness exercises the same byte layout the Go # port targets. SPQR's own incremental_mlkem768 wrapper is pub(crate), so the # harness calls libcrux's public `mlkem768::incremental` API directly and @@ -66,14 +66,13 @@ futures = "0.3" # EncapsState i16s in correct little-endian, so the generated fixture is # host-independent and free of the cryspen/libcrux#1275 byte-swap (which the Go # port handles defensively regardless — see gen_mlkem_incremental). -libcrux-ml-kem = { version = "=0.0.8", default-features = false, features = ["incremental", "mlkem768", "alloc"] } +libcrux-ml-kem = { version = "=0.0.10", default-features = false, features = ["incremental", "mlkem768", "alloc"] } # The SPQR crate itself, pinned to the SAME rev libsignal pulls in transitively -# (git tag v1.5.1, rev f2589fe — unchanged between v0.91.0 and the v0.96.4 pin; -# see Cargo.lock), with `test-utils` +# (git tag v1.6.0; exact commit recorded in Cargo.lock), with `test-utils` # enabled. test-utils is a dependency-free feature flag that re-exports SPQR's # otherwise pub(crate) `encoding` module (GF16 + the polynomial chunk # encoder/decoder). The harness calls encoding::polynomial::PolyEncoder::chunk_at # and the GF16 Mul/Div operators to capture golden BE-u16 chunk byte vectors and # GF16 mul/div triples — oracle leg (c) for T27 Slice B, pinning the big-endian # wire that the erasure property test alone cannot catch. -spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.5.1", features = ["test-utils"] } +spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.6.0", features = ["test-utils"] } diff --git a/compat/rust-harness/README.md b/compat/rust-harness/README.md index 68e990eeb..5873cd7df 100644 --- a/compat/rust-harness/README.md +++ b/compat/rust-harness/README.md @@ -10,9 +10,14 @@ Compatibility harness that wraps upstream behavioral reference oracle for the pure-Go port. It is a **dev/CI-only** crate: nothing in the Go module depends on it, and it is not published. -The upstream dependency is pinned to a fixed tag, **`v0.96.4`** — the Stage-2 -mainline-compat target (T29 advanced it from the Stage-1 pin `v0.91.0`; see ADR -0001). It lives in its own isolated Cargo workspace (`[workspace] members = +The upstream dependencies are pinned to the peeled **`v0.104.0`** release commit +`257105c55a7389ca6b1e85185e2769465e6729f1` (annotated tag object +`03b9987415e6dcb3d83dfde6a16b52d2b59a7b44`, unsigned). This includes +`libsignal-protocol`, `usernames`, and `libsignal-account-keys`, plus their +upstream path dependencies. SPQR is pinned to `v1.6.0`, commit +`06959b4708f9b7b1e94d0f8cc835f3958c077e94`; libcrux-ml-kem is exactly `0.0.10`. +`Cargo.lock` records the complete dependency graph. It lives in its own isolated +Cargo workspace (`[workspace] members = ["."]`) so it is never pulled into a parent workspace, mirroring `rust/protocol/cross-version-testing/Cargo.toml` upstream. @@ -36,22 +41,29 @@ If `protoc` is installed somewhere off `PATH`, point the build at it with the ## Toolchain -`rust-toolchain.toml` pins `nightly-2026-03-23`, matching the toolchain -upstream `v0.96.4` itself pins (the same nightly `v0.91.0` pinned, so the -Stage-2 re-pin needed no toolchain change). `rustup` fetches it on demand. +`rust-toolchain.toml` pins Rust `1.98.1`, matching upstream `v0.104.0`'s root +`rust-toolchain`. With working rustup proxies it is selected automatically. +When Homebrew binaries take precedence, selecting Cargo alone can still run +Homebrew's `rustc`; explicitly select the compiler and documentation tool too: + +```sh +export RUSTC="$(rustup which --toolchain 1.98.1 rustc)" +export RUSTDOC="$(rustup which --toolchain 1.98.1 rustdoc)" +``` ## Usage Build (release): ```sh -cargo build --release +rustup run 1.98.1 cargo build --release --locked ``` ### gen-vectors Prints a deterministic JSON batch of test vectors to stdout. The batch header -records the `seed`; output is byte-identical across runs (seeded ChaCha20). +records the `seed`; output is byte-identical across runs. Most domains use seeded +ChaCha20; account keys use known-test inputs and explicit MFA IV replay bytes. ```sh rust-harness gen-vectors @@ -73,7 +85,7 @@ Domains: - `username-links` — username-link entropy, deterministic IV, encrypted username bytes (`IV || ciphertext || HMAC`), username reservation hash, and upstream-decrypted username from `rust/usernames`. -- `mlkem-incremental` — byte-exact KATs for libcrux 0.0.8's incremental +- `mlkem-incremental` — byte-exact KATs for libcrux 0.0.10's incremental ML-KEM-768 (the KEM SPQR uses): the keygen split (`pk1`/`pk2`/`dk`), two-phase encapsulation (`ct1`, `encaps_state`, `ct2`, `shared_secret`), and decapsulation. `encaps_state` is the raw libcrux state for this host's backend; @@ -82,7 +94,7 @@ Domains: for the pure-Go `internal/mlkem768incr` incremental layer; the generated batch is committed at `internal/mlkem768incr/testdata/libcrux_incremental_mlkem768.json`. -- `spqr-chunks` — golden byte vectors for SPQR v1.5.1's GF(2^16) chunked-transport +- `spqr-chunks` — golden byte vectors for SPQR v1.6.0's GF(2^16) chunked-transport erasure code (the `test-utils` feature exposes its `encoding` module): a set of `chunk_at(i)` outputs (`cases`) pinning the BIG-endian u16 point/coefficient wire, plus GF16 `mul`/`div` triples (`gf_triples`) pinning the field @@ -90,6 +102,65 @@ Domains: the erasure property test alone is blind to a uniformly-wrong endianness, so the golden bytes are required. Committed at `internal/spqr/chunked/testdata/spqr_chunks.json`. +- `account-keys` — upstream account entropy/backup derivations, SVR-key + derivations, PIN master-key HMAC-SHA256-SIV, media key splits, and deterministic + MFA metadata encryption/decryption. See the schema below. + +The dependency pin alone does not upgrade the Go protocol/SPQR implementation or +the provenance of previously committed fixtures. Only +`compat/vectors/account-keys.json` is regenerated here; the remaining protocol, +SPQR, and fixture refresh work is tracked separately. + +### Account-Key Schema And Replay + +All byte strings are lowercase hex. Names/PINs are text; timestamps are unsigned +64-bit JSON integers in milliseconds. Consumers must not decode timestamps +through floating point (the maximum-timestamp case exceeds exact float64 range). +Every derived/encrypted value is generated by the real upstream public API; +only splitting the upstream 64-byte media key into two 32-byte halves is local. + +The legacy `domain`, `seed`, `cases` fields (`entropy_pool`, `svr_key`, +`backup_key`, `aci`, `backup_id`), and `pin_cases` fields (`pin`, `salt`, +`access_key`) retain their original values and v0.96.4 provenance. `_note` +identifies these legacy fields separately from the new v0.104.0 oracle additions +and records the annotated tag object and peeled commit. The historical seed is +an input identifier, not a claim that new vectors came from v0.96.4. + +| Array | Fields | +|---|---| +| `cases` | Legacy fields plus `media_name`, 15-byte `media_id`, 64-byte `media_encryption_key_data`, 32-byte `media_aes_key`/`media_hmac_key`, and corresponding `thumbnail_transit_encryption_key_data`, `thumbnail_aes_key`, `thumbnail_hmac_key` | +| `pin_cases` | Legacy fields plus 32-byte `encryption_key`/`master_key`/`decoded_master_key`, 48-byte `encrypted_master_key`, and `tampered` entries `{offset, encrypted_master_key, accepted}` | +| `svr_cases` | `label`, 32-byte `svr_key`, `registration_lock`, `registration_recovery_password`, `storage_service_key`, `logging_key` | +| `mfa_cases` | `label`, `svr_key`, `name`, `created_at_millis`, 16-byte `iv`, 160-byte `encrypted_metadata`, `decrypted_name`, `decrypted_created_at_millis` | +| `mfa_invalid_cases` | `label`, `svr_key`, `encrypted_metadata`, `accepted` (false); IV/ciphertext/MAC tamper, wrong key, and 159/161-byte lengths | +| `mfa_invalid_name_cases` | `name`, `created_at_millis`, upstream diagnostic `error`; 99 ASCII bytes, 100 UTF-8 bytes, or embedded NUL | + +SVR cases use the account-derived key, all-zero/all-one bytes, and sequential +bytes. PIN cases retain the two original normalized PIN inputs and salts. The +master-key ciphertext is `16-byte synthetic IV || 32-byte ciphertext`. + +MFA cases cover empty name/epoch, millisecond truncation to seconds, the 98-byte +ASCII and multibyte UTF-8 name limits, and `u64::MAX` milliseconds. `FixedRng` +replays exactly the recorded IV; upstream consumes exactly 16 bytes. The blob is +`16-byte IV || 112-byte AES-256-CBC ciphertext || 32-byte HMAC-SHA256`. Generation +asserts successful round trips and upstream rejection of every negative entry. +The upstream name error text is diagnostic only, not a Go error-string contract. +Authenticated malformed padding/protobuf/overflow are covered by independent Go +negative tests; this oracle slice does not generate those fixtures or extend the +protocol commands. + +From this directory, regenerate and verify: + +```sh +rustup run 1.98.1 cargo build --release --locked +./target/release/rust-harness gen-vectors account-keys > ../vectors/account-keys.json +rustup run 1.98.1 cargo test --release --locked +``` + +For a bounded external build directory, set +`CARGO_TARGET_DIR=/private/tmp/signal-task31-rust-target` and run its +`release/rust-harness` binary instead. The fixture replay test compares the full +generated JSON to the committed fixture; a separate test protects legacy outputs. Example: @@ -130,6 +201,5 @@ The chain-key / root-key / message-keys / pqxdh-secret derivations are `pub(crate)` upstream, so the harness reproduces them with the same pinned crate versions (`hkdf`, `hmac`, `sha2` — matching upstream's pins). The formulas are taken verbatim from `rust/protocol/src/ratchet/keys.rs` and `ratchet.rs` at -the v0.96.4 tag, which remain the contract (these version-stable formulas are -unchanged from v0.91.0 — the hkdf vectors are byte-identical across the re-pin). +the v0.104.0 commit, which remains the contract. Every other domain calls the genuine public API. diff --git a/compat/rust-harness/rust-toolchain.toml b/compat/rust-harness/rust-toolchain.toml index 2b56de1e0..772bf5d01 100644 --- a/compat/rust-harness/rust-toolchain.toml +++ b/compat/rust-harness/rust-toolchain.toml @@ -3,10 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only # # Pins the toolchain used to build the compat harness against upstream -# libsignal-protocol v0.96.4 (the Stage-2 mainline-compat target). v0.96.4's own -# rust-toolchain pins the SAME nightly-2026-03-23 as v0.91.0 did, so the T29 -# re-pin needs no toolchain bump (the harness must compile under the same -# toolchain as the upstream code it links). rustup fetches it on demand. +# libsignal v0.104.0. Matches upstream's root rust-toolchain. [toolchain] -channel = "nightly-2026-03-23" +channel = "1.98.1" +profile = "minimal" diff --git a/compat/rust-harness/src/main.rs b/compat/rust-harness/src/main.rs index 7ec755696..4942c7354 100644 --- a/compat/rust-harness/src/main.rs +++ b/compat/rust-harness/src/main.rs @@ -5,20 +5,20 @@ //! Rust compatibility harness for the pure-Go libsignal port. //! //! Wraps upstream `libsignal-protocol` and related crates pinned to tag -//! v0.96.4 and exposes two modes: +//! v0.104.0 and exposes two modes: //! //! * `gen-vectors ` — prints a deterministic JSON test-vector batch -//! to stdout. Output is seeded with a fixed `rand_chacha` seed recorded in -//! the batch header, so re-running produces byte-identical vectors. +//! to stdout. Most domains use the fixed `rand_chacha` seed recorded in +//! the batch header; account keys replay known inputs and explicit IVs. //! * `interop` — a line-delimited JSON-RPC loop over stdin/stdout. Each input //! line is one request object `{"method": "...", "params": {...}}`; each //! output line is one response. Unknown methods produce an error response, //! never a crash, and the loop continues. //! //! Domains for `gen-vectors`: `curve`, `kem-decaps`, `hkdf`, `messages`, -//! `fingerprint`, `groups`, `username-links`. +//! `fingerprint`, `groups`, `username-links`, `account-keys`. //! -//! The behavioral contract is the v0.96.4 upstream source. Most domains call +//! The behavioral contract is the v0.104.0 upstream source. Most domains call //! the genuine public API directly. The `hkdf` domain reproduces the chain //! key / root key / message key / pqxdh-secret derivations, which are //! `pub(crate)` upstream, against the same pinned crate versions (`hkdf`, @@ -31,13 +31,17 @@ use std::time::SystemTime; use futures_util::FutureExt; use hkdf::Hkdf; -use hmac::{Hmac, Mac}; +use hmac::{Hmac, KeyInit as _, Mac}; use rand::rngs::OsRng; use rand::{Rng as _, SeedableRng, TryRngCore as _}; use rand_chacha::ChaCha20Rng; use serde_json::{json, Value}; use sha2::Sha256; +use libsignal_account_keys::{ + AccountEntropyPool, BackupKey, EncryptedMfaMetadata, MfaMetadata, PinHash, SvrKey, +}; + use libsignal_protocol::{ create_sender_key_distribution_message, group_decrypt, group_encrypt, kem, message_decrypt, message_encrypt, process_prekey_bundle, process_sender_key_distribution_message, @@ -109,8 +113,8 @@ impl rand_core::RngCore for FixedRng { } } -// rand_core 0.9 CryptoRng is a marker trait. The fixed bytes here are drawn -// from a CSPRNG upstream, so asserting the marker is sound for test-vector use. +// Test-only marker permits replaying recorded signing nonces and MFA IVs. +// FixedRng is deterministic fixture input, not production entropy. impl rand_core::CryptoRng for FixedRng {} fn hex(bytes: &[u8]) -> String { @@ -217,8 +221,8 @@ fn gen_vectors(domain: &str) -> Result<(), String> { "_note".into(), json!({ "encaps_state_backend": "libcrux portable (simd128/simd256 disabled); raw == fixed on all hosts", - "libcrux_ml_kem": "0.0.8 (registry, =0.0.8 in compat/rust-harness/Cargo.toml; checksum-pinned in Cargo.lock)", - "spqr_reference": "1.5.1 (git tag v1.5.1, rev f2589fef855c10f39d72634dab3d14654dd410bf; issue-1275 fix reproduced inline in gen_mlkem_incremental since SPQR's wrapper is pub(crate))", + "libcrux_ml_kem": "0.0.10 (registry, =0.0.10 in compat/rust-harness/Cargo.toml; checksum-pinned in Cargo.lock)", + "spqr_reference": "1.6.0 (git tag v1.6.0, rev 06959b4708f9b7b1e94d0f8cc835f3958c077e94; issue-1275 fix reproduced inline in gen_mlkem_incremental since SPQR's wrapper is pub(crate))", "issue_1275": "https://github.com/cryspen/libcrux/issues/1275 — SIMD-backend EncapsState i16 endianness", }), ); @@ -226,13 +230,13 @@ fn gen_vectors(domain: &str) -> Result<(), String> { } "spqr-chunks" => { // Golden BE-u16 chunk vectors + GF16 mul/div triples captured from - // the SPQR v1.5.1 crate (test-utils feature). Pins the big-endian + // the SPQR v1.6.0 crate (test-utils feature). Pins the big-endian // point/coefficient wire serialization that the erasure property // test cannot see (encoder+decoder share their convention). obj.insert( "_note".into(), json!({ - "spqr_reference": "1.5.1 (git tag v1.5.1, rev f2589fef855c10f39d72634dab3d14654dd410bf, test-utils feature)", + "spqr_reference": "1.6.0 (git tag v1.6.0, rev 06959b4708f9b7b1e94d0f8cc835f3958c077e94, test-utils feature)", "endianness": "GF16 points/coefficients serialize BIG-endian u16 (encoding/polynomial.rs Pt::serialize, Poly::serialize, chunk_at) — OPPOSITE of the KEM EncapsState LE-i16", "gf_poly": "0x1100b (x^16+x^12+x^3+x+1)", }), @@ -243,10 +247,13 @@ fn gen_vectors(domain: &str) -> Result<(), String> { "username-links" => { obj.insert("cases".into(), Value::Array(gen_username_links())); } + "account-keys" => { + batch = gen_account_keys(); + } other => { return Err(format!( "unknown domain {other:?}; \ - expected curve|kem-decaps|hkdf|messages|fingerprint|sessions|groups|sealedsender|mlkem-incremental|spqr-chunks|username-links" + expected curve|kem-decaps|hkdf|messages|fingerprint|sessions|groups|sealedsender|mlkem-incremental|spqr-chunks|username-links|account-keys" )); } }; @@ -256,6 +263,241 @@ fn gen_vectors(domain: &str) -> Result<(), String> { Ok(()) } +/// Task31's account oracle calls upstream APIs, including encryption and +/// authentication. The legacy seed identifies the retained known-test inputs; +/// new MFA IVs are explicit replay bytes, not draws from that seed. +fn gen_account_keys() -> Value { + let pool: AccountEntropyPool = + "dtjs858asj6tv0jzsqrsmj0ubp335pisj98e9ssnss8myoc08drhtcktyawvx45l" + .parse() + .expect("known entropy pool"); + let svr_bytes = pool.derive_svr_key(); + let backup = BackupKey::derive_from_account_entropy_pool(&pool); + let aci_bytes: [u8; 16] = hex::decode("659aa5f4a28dfcc11ea1b997537a3d95") + .expect("ACI hex") + .try_into() + .expect("ACI length"); + let media_name = "task31-account-media"; + let media_id = backup.derive_media_id(media_name); + let media_key = backup.derive_media_encryption_key_data(&media_id); + let thumbnail_key = backup.derive_thumbnail_transit_encryption_key_data(&media_id); + + let svr_cases: Vec = [ + ("account-entropy", svr_bytes), + ("zero", [0; 32]), + ("ones", [0xff; 32]), + ("sequence", std::array::from_fn(|i| i as u8)), + ] + .into_iter() + .map(|(label, bytes)| { + let key = SvrKey::new(bytes); + json!({ + "label": label, + "svr_key": hex(&bytes), + "registration_lock": hex(&key.derive_registration_lock()), + "registration_recovery_password": hex(&key.derive_registration_recovery_password()), + "storage_service_key": hex(&key.derive_storage_service_key()), + "logging_key": hex(&key.derive_logging_key()), + }) + }) + .collect(); + + let pin_cases: Vec = ["password", "anotherpassword"] + .into_iter() + .enumerate() + .map(|(i, pin)| { + let salt = std::array::from_fn(|j| (i * 32 + j) as u8); + let hash = PinHash::create(pin.as_bytes(), &salt).expect("PIN hash"); + let master_key = if i == 0 { svr_bytes } else { [0xff; 32] }; + let encrypted = hash.encode_master_key(&master_key); + let decoded = hash + .decode_master_key(&encrypted) + .expect("master key authenticates"); + assert_eq!(decoded, master_key); + let tampered: Vec = [0, 16, 47].into_iter().map(|offset| { + let mut blob = encrypted; + blob[offset] ^= 1; + let accepted = hash.decode_master_key(&blob).is_some(); + assert!(!accepted, "tampered PIN master key at {offset}"); + json!({"offset": offset, "encrypted_master_key": hex(&blob), "accepted": accepted}) + }).collect(); + json!({ + "pin": pin, + "salt": hex(&salt), + "access_key": hex(&hash.access_key), + "encryption_key": hex(&hash.encryption_key), + "master_key": hex(&master_key), + "encrypted_master_key": hex(&encrypted), + "decoded_master_key": hex(&decoded), + "tampered": tampered, + }) + }) + .collect(); + + let svr = SvrKey::new(svr_bytes); + let mfa_cases: Vec = [ + ("empty-epoch", String::new(), 0), + ( + "second-truncation", + "Task31 security key".to_owned(), + 1_782_484_792_999, + ), + ("ascii-limit", "a".repeat(98), 1_782_484_792_000), + ("utf8-limit", "\u{00e9}".repeat(49), 1_782_484_792_000), + ("timestamp-limit", String::new(), u64::MAX), + ] + .into_iter() + .enumerate() + .map(|(i, (label, name, millis))| { + let metadata = MfaMetadata::new(name.clone(), Timestamp::from_epoch_millis(millis)) + .expect("valid metadata"); + let iv: [u8; 16] = std::array::from_fn(|j| (i * 16 + j) as u8); + let mut rng = FixedRng::new(iv.to_vec()); + let encrypted = metadata.encrypt(&svr, &mut rng); + assert_eq!(rng.pos, 16, "MFA draws only the IV"); + let decrypted = encrypted.decrypt(&svr).expect("MFA authenticates"); + assert_eq!(decrypted.name(), name); + assert_eq!(decrypted.created_at().epoch_millis(), millis / 1000 * 1000); + json!({ + "label": label, + "svr_key": hex(&svr_bytes), + "name": name, + "created_at_millis": millis, + "iv": hex(&iv), + "encrypted_metadata": hex(encrypted.as_bytes()), + "decrypted_name": decrypted.name(), + "decrypted_created_at_millis": decrypted.created_at().epoch_millis(), + }) + }) + .collect(); + + let original = hex::decode( + mfa_cases[1]["encrypted_metadata"] + .as_str() + .expect("blob hex"), + ) + .expect("blob"); + let mut mfa_invalid_cases = Vec::new(); + for (label, offset) in [("iv", 0), ("ciphertext", 16), ("mac", 159)] { + let mut blob = original.clone(); + blob[offset] ^= 1; + let accepted = EncryptedMfaMetadata::try_from(blob.as_slice()) + .expect("160 bytes") + .decrypt(&svr) + .is_ok(); + assert!(!accepted, "MFA rejects {label} tamper"); + mfa_invalid_cases.push(json!({ + "label": label, "svr_key": hex(&svr_bytes), + "encrypted_metadata": hex(&blob), "accepted": accepted, + })); + } + for len in [159, 161] { + let mut blob = original.clone(); + blob.resize(len, 0); + let accepted = EncryptedMfaMetadata::try_from(blob.as_slice()).is_ok(); + assert!(!accepted, "MFA rejects length {len}"); + mfa_invalid_cases.push(json!({ + "label": format!("length-{len}"), "svr_key": hex(&svr_bytes), + "encrypted_metadata": hex(&blob), "accepted": accepted, + })); + } + let wrong_key = [0; 32]; + let accepted = EncryptedMfaMetadata::try_from(original.as_slice()) + .expect("160 bytes") + .decrypt(&SvrKey::new(wrong_key)) + .is_ok(); + assert!(!accepted, "MFA rejects wrong key"); + mfa_invalid_cases.push(json!({ + "label": "wrong-key", "svr_key": hex(&wrong_key), + "encrypted_metadata": hex(&original), "accepted": accepted, + })); + + let mfa_invalid_name_cases: Vec = [ + "a".repeat(99), + "\u{00e9}".repeat(50), + "before\0after".to_owned(), + ] + .into_iter() + .map(|name| { + let error = MfaMetadata::new(name.clone(), Timestamp::from_epoch_millis(0)) + .expect_err("invalid MFA name"); + json!({"name": name, "created_at_millis": 0, "error": error.to_string()}) + }) + .collect(); + + json!({ + "domain": "account-keys", + "seed": "upstream-v0.96.4-account-keys-known-tests", + "_note": { + "legacy_upstream_tag": "v0.96.4", + "legacy_fields": "seed; cases entropy_pool/svr_key/backup_key/aci/backup_id; pin_cases pin/salt/access_key", + "upstream_tag": "v0.104.0", + "upstream_tag_object": "03b9987415e6dcb3d83dfde6a16b52d2b59a7b44", + "upstream_commit": "257105c55a7389ca6b1e85185e2769465e6729f1", + "oracle": "libsignal-account-keys public APIs; Rust 1.98.1", + "seed": "Legacy input identifier retained; MFA cases replay the explicit 16-byte IV through FixedRng.", + }, + "cases": [{ + "entropy_pool": pool.to_string(), + "svr_key": hex(&svr_bytes), + "backup_key": hex(&backup.0), + "aci": hex(&aci_bytes), + "backup_id": hex(&backup.derive_backup_id(&Aci::from_uuid_bytes(aci_bytes)).0), + "media_name": media_name, + "media_id": hex(&media_id), + "media_encryption_key_data": hex(&media_key), + "media_aes_key": hex(&media_key[..32]), + "media_hmac_key": hex(&media_key[32..]), + "thumbnail_transit_encryption_key_data": hex(&thumbnail_key), + "thumbnail_aes_key": hex(&thumbnail_key[..32]), + "thumbnail_hmac_key": hex(&thumbnail_key[32..]), + }], + "pin_cases": pin_cases, + "svr_cases": svr_cases, + "mfa_cases": mfa_cases, + "mfa_invalid_cases": mfa_invalid_cases, + "mfa_invalid_name_cases": mfa_invalid_name_cases, + }) +} + +#[cfg(test)] +mod account_oracle_tests { + use super::*; + + #[test] + fn account_keys_fixture_replays() { + let fixture: Value = serde_json::from_str(include_str!("../../vectors/account-keys.json")) + .expect("account fixture JSON"); + assert_eq!(gen_account_keys(), fixture); + } + + #[test] + fn account_keys_legacy_outputs_are_preserved() { + let batch = gen_account_keys(); + assert_eq!(batch["seed"], "upstream-v0.96.4-account-keys-known-tests"); + assert_eq!( + batch["cases"][0]["svr_key"], + "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + ); + assert_eq!( + batch["cases"][0]["backup_key"], + "ea26a2ddb5dba5ef9e34e1b8dea1f5ae7f255306a6d2d883e542306eaa9fe985" + ); + assert_eq!( + batch["cases"][0]["backup_id"], + "8a624fbc45379043f39f1391cddc5fe8" + ); + assert_eq!( + batch["pin_cases"][0]["access_key"], + "ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8" + ); + assert_eq!( + batch["pin_cases"][1]["access_key"], + "301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b" + ); + } +} + /// curve domain: XEdDSA sign/verify with a deterministic 64-byte nonce (the /// nonce is drawn from the seeded CSPRNG, making the signature reproducible) /// and X25519 ECDH agreement. @@ -901,10 +1143,10 @@ fn gen_sealedsender() -> Vec { // registry keyed by an opaque string handle, so Go can run a whole conversation // (handshake, then many encrypt/decrypt turns) against the same Rust peer. // -// v0.91.0 sessions are PQXDH/v4 only (X3DH/v3 is removed upstream — see -// session.rs "X3DH no longer supported"). v0.91.0 ships the Sparse Post-Quantum -// Ratchet (spqr v1.5.1): initialize_{alice,bob}_session call -// spqr::initial_state(version V1, min_version V0) unconditionally — there is no +// v0.104.0 sessions are PQXDH/v4 only (X3DH/v3 is removed upstream — see +// session.rs "X3DH no longer supported"). v0.104.0 ships the Sparse Post-Quantum +// Ratchet (spqr v1.6.0): initialize_{alice,bob}_session call +// spqr::initial_state(version V1, min_version V1) unconditionally — there is no // UsePQRatchet flag at this tag — so sessions negotiate SPQR and the resulting // v4 SignalMessages carry pq_ratchet bytes that the Go port mixes in (T28). The // InMem stores never actually await, so the async API is driven synchronously @@ -918,7 +1160,7 @@ thread_local! { static STORES: RefCell> = RefCell::new(HashMap::new()); } -// --- mlkem-incremental domain: libcrux 0.0.8 incremental ML-KEM-768 (the SPQR +// --- mlkem-incremental domain: libcrux 0.0.10 incremental ML-KEM-768 (the SPQR // KEM), oracle 3 for the pure-Go internal/mlkem768incr incremental layer. --- /// Byte sizes of the libcrux incremental ML-KEM-768 wire artifacts (K=3). These @@ -934,7 +1176,7 @@ const MLKEM_INCR_STATE_LEN: usize = 2080; // r̂(3·512) ‖ error2(512) ‖ ran const MLKEM_INCR_SS_LEN: usize = 32; /// Reproduces SPQR's `potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275` -/// (SparsePostQuantumRatchet v1.5.1 src/incremental_mlkem768.rs), which is +/// (SparsePostQuantumRatchet v1.6.0 src/incremental_mlkem768.rs), which is /// `pub(crate)` in the spqr crate and so not callable here. libcrux's SIMD /// backends (NEON/AVX2) serialize the per-coefficient i16s in `EncapsState` with /// swapped endianness (cryspen/libcrux#1275); the portable backend is correct. @@ -980,7 +1222,7 @@ fn mlkem_incr_fix_state_1275(es: &[u8]) -> Vec { out } -/// mlkem-incremental domain: byte-exact KATs for the libcrux 0.0.8 incremental +/// mlkem-incremental domain: byte-exact KATs for the libcrux 0.0.10 incremental /// ML-KEM-768 split (the KEM SPQR uses). Each case is generated from a fixed /// 64-byte keygen seed and a fixed 32-byte encapsulation message (both drawn /// from the seeded CSPRNG, so the batch is reproducible), exercising the full @@ -1104,7 +1346,7 @@ fn gen_username_links() -> Vec { } // --- spqr-chunks domain: golden BE-u16 chunk vectors + GF16 mul/div triples -// captured from SPQR v1.5.1 (test-utils), oracle leg (c) for T27 Slice B. --- +// captured from SPQR v1.6.0 (test-utils), oracle leg (c) for T27 Slice B. --- /// Number of golden chunk cases and GF16 triples to emit. Sized to cover the /// literal-data path (low indices), the interpolation path (high indices), and a diff --git a/compat/upstream_pin_test.go b/compat/upstream_pin_test.go new file mode 100644 index 000000000..75c91abef --- /dev/null +++ b/compat/upstream_pin_test.go @@ -0,0 +1,132 @@ +package compat + +import ( + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func upstreamPinFixture(t *testing.T, manifest string) (root, script string) { + t.Helper() + if _, err := exec.LookPath("bash"); err != nil { + t.Skip("upstream updater requires bash") + } + if _, err := exec.LookPath("jq"); err != nil { + t.Skip("upstream updater requires jq") + } + root = t.TempDir() + scriptDir := filepath.Join(root, "scripts") + harnessDir := filepath.Join(root, "compat", "rust-harness") + binDir := filepath.Join(root, "bin") + for _, dir := range []string{scriptDir, harnessDir, binDir} { + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + } + raw, err := os.ReadFile("../scripts/update-upstream-pin.sh") + if err != nil { + t.Fatal(err) + } + script = filepath.Join(scriptDir, "update-upstream-pin.sh") + if err := os.WriteFile(script, raw, 0o600); err != nil { // #nosec G703 -- fixed child path inside t.TempDir, never user input. + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(harnessDir, "Cargo.toml"), []byte(manifest), 0o600); err != nil { + t.Fatal(err) + } + // Substitute only Cargo metadata, not the updater being exercised. + const cargo = `#!/bin/sh +printf '%s\n' '{"packages":[{"name":"rust-harness","metadata":{"libsignal":{"upstream_tag":"v0.104.0"}},"dependencies":[{"name":"libsignal-protocol","source":"git+https://github.com/signalapp/libsignal?rev=257105c55a7389ca6b1e85185e2769465e6729f1"}]}]}' +` + if err := os.WriteFile(filepath.Join(binDir, "cargo"), []byte(cargo), 0o700); err != nil { // #nosec G306 -- owner-only executable test dependency. + t.Fatal(err) + } + return root, script +} + +func runUpstreamPin(t *testing.T, root, script, tag string) ([]byte, error) { + t.Helper() + binDir := filepath.Join(root, "bin") + cmd := exec.CommandContext(t.Context(), "bash", script, tag) // #nosec G204 -- local updater and fixed test tags. + cmd.Env = append(os.Environ(), "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH")) + return cmd.CombinedOutput() +} + +func TestUpstreamPinReadsImmutableReleaseMetadata(t *testing.T) { + for _, pin := range []string{`rev = "257105c55a7389ca6b1e85185e2769465e6729f1"`, `tag = "v0.104.0"`} { + manifest := `libsignal-protocol = { git = "https://github.com/signalapp/libsignal", ` + pin + " }\n" + root, script := upstreamPinFixture(t, manifest) + if strings.HasPrefix(pin, "tag") { + if err := os.WriteFile(filepath.Join(root, "bin", "cargo"), []byte("#!/bin/sh\nexit 88\n"), 0o700); err != nil { // #nosec G306 -- owner-only executable test dependency. + t.Fatal(err) + } + } + out, err := runUpstreamPin(t, root, script, "v0.104.0") + if err != nil || !strings.Contains(string(out), "already pinned to v0.104.0") { + t.Fatalf("pin no-op failed: %v\n%s", err, out) + } + unchanged, err := os.ReadFile(filepath.Join(root, "compat", "rust-harness", "Cargo.toml")) // #nosec G304 -- fixed child of the test-owned temporary directory. + if err != nil || string(unchanged) != manifest { + t.Fatal("same-release updater modified its pin") + } + } +} + +func TestUpstreamPinMovesAllImmutableDependencies(t *testing.T) { + const current = "257105c55a7389ca6b1e85185e2769465e6729f1" + const next = "2222222222222222222222222222222222222222" + manifest := "[package.metadata.libsignal]\nupstream_tag = \"v0.104.0\"\n[dependencies]\n" + for _, name := range []string{"libsignal-protocol", "libsignal-account-keys", "usernames"} { + manifest += name + ` = { git = "https://github.com/signalapp/libsignal", rev = "` + current + "\" }\n" + } + root, script := upstreamPinFixture(t, manifest) + for _, path := range []string{ + "README.md", "compat/README.md", "compat/coverage_manifest.json", "compat/coverage_manifest_test.go", + "proofreport/report_test.go", "accountkeys/accountkeys_test.go", "usernames/usernames_test.go", + "compat/rust-harness/README.md", "compat/session_interop_test.go", ".github/workflows/compat.yml", ".github/workflows/compat-drift.yml", + } { + file := filepath.Join(root, filepath.FromSlash(path)) + if err := os.MkdirAll(filepath.Dir(file), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file, nil, 0o600); err != nil { + t.Fatal(err) + } + } + const gh = `#!/bin/sh +case "$2" in +*/git/ref/tags/*) printf '%s\n' '{"sha":"3333333333333333333333333333333333333333","type":"tag"}' ;; +*/git/tags/*) printf '%s\n' '{"sha":"2222222222222222222222222222222222222222","type":"commit"}' ;; +*) exit 90 ;; +esac +` + if err := os.WriteFile(filepath.Join(root, "bin", "gh"), []byte(gh), 0o700); err != nil { // #nosec G306 -- owner-only executable test dependency. + t.Fatal(err) + } + cargoPath := filepath.Join(root, "bin", "cargo") + cargo, err := os.ReadFile(cargoPath) // #nosec G304 -- fixed child of the test-owned temporary directory. + if err != nil { + t.Fatal(err) + } + // Stop at the build boundary; no network, dependency resolution or Go tests. + cargo = bytes.Replace(cargo, []byte("#!/bin/sh\n"), []byte("#!/bin/sh\nif [ \"$1\" != metadata ]; then exit 77; fi\n"), 1) + if err := os.WriteFile(cargoPath, cargo, 0o700); err != nil { // #nosec G306 G703 -- owner-only executable at a fixed t.TempDir child, never user input. + t.Fatal(err) + } + out, err := runUpstreamPin(t, root, script, "v0.105.0") + var exit *exec.ExitError + if !errors.As(err, &exit) || exit.ExitCode() != 77 { + t.Fatalf("updater failed before mocked build boundary: %v\n%s", err, out) + } + updated, err := os.ReadFile(filepath.Join(root, "compat", "rust-harness", "Cargo.toml")) // #nosec G304 -- fixed child of the test-owned temporary directory. + if err != nil { + t.Fatal(err) + } + if strings.Count(string(updated), next) != 3 || strings.Contains(string(updated), current) || !strings.Contains(string(updated), `upstream_tag = "v0.105.0"`) { + t.Fatal("updater did not move all three libsignal revisions and release metadata together") + } +} diff --git a/compat/vectors/account-keys.json b/compat/vectors/account-keys.json index 91db2dc2e..b2861d98e 100644 --- a/compat/vectors/account-keys.json +++ b/compat/vectors/account-keys.json @@ -1,25 +1,225 @@ { - "domain": "account-keys", - "seed": "upstream-v0.96.4-account-keys-known-tests", + "_note": { + "legacy_fields": "seed; cases entropy_pool/svr_key/backup_key/aci/backup_id; pin_cases pin/salt/access_key", + "legacy_upstream_tag": "v0.96.4", + "oracle": "libsignal-account-keys public APIs; Rust 1.98.1", + "seed": "Legacy input identifier retained; MFA cases replay the explicit 16-byte IV through FixedRng.", + "upstream_commit": "257105c55a7389ca6b1e85185e2769465e6729f1", + "upstream_tag": "v0.104.0", + "upstream_tag_object": "03b9987415e6dcb3d83dfde6a16b52d2b59a7b44" + }, "cases": [ { + "aci": "659aa5f4a28dfcc11ea1b997537a3d95", + "backup_id": "8a624fbc45379043f39f1391cddc5fe8", + "backup_key": "ea26a2ddb5dba5ef9e34e1b8dea1f5ae7f255306a6d2d883e542306eaa9fe985", "entropy_pool": "dtjs858asj6tv0jzsqrsmj0ubp335pisj98e9ssnss8myoc08drhtcktyawvx45l", + "media_aes_key": "8a99ce3c134d0a3517ebc770a91569168da76009c4350038614bb790efdbcb65", + "media_encryption_key_data": "8a99ce3c134d0a3517ebc770a91569168da76009c4350038614bb790efdbcb651839b63a4e64d77ff0e1e93414e6c66836ed933467bd95386842f8c2a442d53d", + "media_hmac_key": "1839b63a4e64d77ff0e1e93414e6c66836ed933467bd95386842f8c2a442d53d", + "media_id": "ea233dec048a91eb835bbdb62185e3", + "media_name": "task31-account-media", "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1", - "backup_key": "ea26a2ddb5dba5ef9e34e1b8dea1f5ae7f255306a6d2d883e542306eaa9fe985", - "aci": "659aa5f4a28dfcc11ea1b997537a3d95", - "backup_id": "8a624fbc45379043f39f1391cddc5fe8" + "thumbnail_aes_key": "cdbbf0fbe4ca8eb1667cbb619e2bb38b71d46f9eb8e6bb3f123c6d0458ecdf3d", + "thumbnail_hmac_key": "08762e2bebf46a3660e8dc1db1e36661d34183e5ec34ecc3b57e6fa610d65616", + "thumbnail_transit_encryption_key_data": "cdbbf0fbe4ca8eb1667cbb619e2bb38b71d46f9eb8e6bb3f123c6d0458ecdf3d08762e2bebf46a3660e8dc1db1e36661d34183e5ec34ecc3b57e6fa610d65616" + } + ], + "domain": "account-keys", + "mfa_cases": [ + { + "created_at_millis": 0, + "decrypted_created_at_millis": 0, + "decrypted_name": "", + "encrypted_metadata": "000102030405060708090a0b0c0d0e0f5ff0c34680fb6e1300065eaa1a0399e4cd30f214759b88bf6a4e79fed640014502c10d412fb4881f2611b680b297cfe976673d2c102bc4c91e8dcb6c5190265bb9e2661ec46f7e44b27b3fd78d5cb945576fcae178f25d5ab022fa62f0d54f1587697e06cf8363d3c14f2a85b93dc991fc5e71e6fb5159574afe0e4244a45299059bf66feebd6673489fde592c2ec625", + "iv": "000102030405060708090a0b0c0d0e0f", + "label": "empty-epoch", + "name": "", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "created_at_millis": 1782484792999, + "decrypted_created_at_millis": 1782484792000, + "decrypted_name": "Task31 security key", + "encrypted_metadata": "101112131415161718191a1b1c1d1e1f8be1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29b6", + "iv": "101112131415161718191a1b1c1d1e1f", + "label": "second-truncation", + "name": "Task31 security key", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "created_at_millis": 1782484792000, + "decrypted_created_at_millis": 1782484792000, + "decrypted_name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "encrypted_metadata": "202122232425262728292a2b2c2d2e2fbd914642088b05d61226760a3f205122d2abe5a036af6f5ff03469e8e3e070cd084ee0e8a5e097fa898ce4eb9ba7a8afa271533951de51626660be1d6f05bf2044658fcb56b4e9019773db0b31d7b952bd3e0fcf813a09383d8a3b0721750449fab5bfb2bf1144ef5aa77b64f41e4d218646e69e595529902173ed21ddf0628b755091bd33783aa251c9c976f67ca5ac", + "iv": "202122232425262728292a2b2c2d2e2f", + "label": "ascii-limit", + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "created_at_millis": 1782484792000, + "decrypted_created_at_millis": 1782484792000, + "decrypted_name": "ééééééééééééééééééééééééééééééééééééééééééééééééé", + "encrypted_metadata": "303132333435363738393a3b3c3d3e3f9b0244ab43658e17c8620cda8084e6505ff1cc89200a975e27d1a092cc2571df126d7811d8e622339f588413868f532166f8e91d8a23512d45300ca0c32539125ed7ccc7bdc91d639422496cde70d847995243d8e239f7f0a12de03b3c82c810e2b420e99c5207eb0f67943913d0f461058ab7b780eb320fc7b7bfb08c8ff68e8829bd6eb976d092036482bd85e7f1cc", + "iv": "303132333435363738393a3b3c3d3e3f", + "label": "utf8-limit", + "name": "ééééééééééééééééééééééééééééééééééééééééééééééééé", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "created_at_millis": 18446744073709551615, + "decrypted_created_at_millis": 18446744073709551000, + "decrypted_name": "", + "encrypted_metadata": "404142434445464748494a4b4c4d4e4fd9a27250f17e816546cd2539fa3d88af3374fe28a710587b54f2d18200efab65ef031f3aa57c25da9c47e80331d24fafb4ea60952e1d06e8a29bbeb291dfd88fa3dfa4538a017273b2609e596e4a8c5a1ba5dcf83285f2bec48ac645d9dded62c3da883a5f90abfca2232a1cae340d4371c8e03fbbe7eb37aaec0fa80e10320bd396b19c479a75abff79f13fe13db868", + "iv": "404142434445464748494a4b4c4d4e4f", + "label": "timestamp-limit", + "name": "", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + } + ], + "mfa_invalid_cases": [ + { + "accepted": false, + "encrypted_metadata": "111112131415161718191a1b1c1d1e1f8be1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29b6", + "label": "iv", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "accepted": false, + "encrypted_metadata": "101112131415161718191a1b1c1d1e1f8ae1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29b6", + "label": "ciphertext", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "accepted": false, + "encrypted_metadata": "101112131415161718191a1b1c1d1e1f8be1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29b7", + "label": "mac", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "accepted": false, + "encrypted_metadata": "101112131415161718191a1b1c1d1e1f8be1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29", + "label": "length-159", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "accepted": false, + "encrypted_metadata": "101112131415161718191a1b1c1d1e1f8be1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29b600", + "label": "length-161", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "accepted": false, + "encrypted_metadata": "101112131415161718191a1b1c1d1e1f8be1754cd84449b4acbf34bd7de5cfd1351b055e3280b108259e0ed51b5a896654425354866117a780b3201b80b06cd2b2e072d27a57d3ec904de734bf23b1035431fa7009823e2bf99a355a9486002998d8e7b0c5e4302c59704d6a3918fba810e8060396bd521649dfbb71c59515be020af7d189fc94ed0515eeeb6e3287082429599e9ba77412d7601e8e994a29b6", + "label": "wrong-key", + "svr_key": "0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "mfa_invalid_name_cases": [ + { + "created_at_millis": 0, + "error": "The MFA key name is longer than the maximum of 98 bytes of UTF-8", + "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "created_at_millis": 0, + "error": "The MFA key name is longer than the maximum of 98 bytes of UTF-8", + "name": "éééééééééééééééééééééééééééééééééééééééééééééééééé" + }, + { + "created_at_millis": 0, + "error": "The MFA key name contains U+0000", + "name": "before\u0000after" } ], "pin_cases": [ { + "access_key": "ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8", + "decoded_master_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1", + "encrypted_master_key": "0861a7b5dc31fc8dc2d333de020cc1de167f29e814050314a9bcdbc1b43968d18435e47dc87be602896e3164b7f90446", + "encryption_key": "44652df80490fc66bb864a9e638b2f7dc9e20649671dd66bbb9c37bee2bfecf1", + "master_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1", "pin": "password", "salt": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", - "access_key": "ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8" + "tampered": [ + { + "accepted": false, + "encrypted_master_key": "0961a7b5dc31fc8dc2d333de020cc1de167f29e814050314a9bcdbc1b43968d18435e47dc87be602896e3164b7f90446", + "offset": 0 + }, + { + "accepted": false, + "encrypted_master_key": "0861a7b5dc31fc8dc2d333de020cc1de177f29e814050314a9bcdbc1b43968d18435e47dc87be602896e3164b7f90446", + "offset": 16 + }, + { + "accepted": false, + "encrypted_master_key": "0861a7b5dc31fc8dc2d333de020cc1de167f29e814050314a9bcdbc1b43968d18435e47dc87be602896e3164b7f90447", + "offset": 47 + } + ] }, { + "access_key": "301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b", + "decoded_master_key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "encrypted_master_key": "e4d510dcd044127dfe4feec37cb3a249daf1768a22233f7948c0c67e9e0820910eb4e3068cb816d5c202c66f54ddc023", + "encryption_key": "b6f16aa0591732e339b7e99cdd5fd6586a1c285c9d66876947fd82f66ed99757", + "master_key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "pin": "anotherpassword", "salt": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", - "access_key": "301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b" + "tampered": [ + { + "accepted": false, + "encrypted_master_key": "e5d510dcd044127dfe4feec37cb3a249daf1768a22233f7948c0c67e9e0820910eb4e3068cb816d5c202c66f54ddc023", + "offset": 0 + }, + { + "accepted": false, + "encrypted_master_key": "e4d510dcd044127dfe4feec37cb3a249dbf1768a22233f7948c0c67e9e0820910eb4e3068cb816d5c202c66f54ddc023", + "offset": 16 + }, + { + "accepted": false, + "encrypted_master_key": "e4d510dcd044127dfe4feec37cb3a249daf1768a22233f7948c0c67e9e0820910eb4e3068cb816d5c202c66f54ddc022", + "offset": 47 + } + ] + } + ], + "seed": "upstream-v0.96.4-account-keys-known-tests", + "svr_cases": [ + { + "label": "account-entropy", + "logging_key": "24f7ffa4b1cec8f4799525149bb54ac7ef9dc9e3e049fbfbe6616d3e8d6f5cb0", + "registration_lock": "d9ae8235c0870ee8d34720ad029b9e46db4c9ac687687868998fca2ce46001e0", + "registration_recovery_password": "2cb4b0ace626e4800ba2c7db5d1506ef7bf66f8708b35bce8dc2f33ac0ea1dc0", + "storage_service_key": "9b9d99d09d8af9f6bdc66a8549c1e4bb1520f1ddbe494c7effc376f6639f8b3d", + "svr_key": "cdfecb856b148ca1c7f7557904f1ec698d0ccc4d4d68ed4c58c74a21e5c1c6c1" + }, + { + "label": "zero", + "logging_key": "478c0913c1a87e2ca7c9b79a292ca894e453ded515a85e9177bab11f476a833a", + "registration_lock": "1d2ebfb68c3b6321d21bf5b4410146fa978399bbd6de677f9cf4f52df58e1a8f", + "registration_recovery_password": "7c8ff82f2a823b07b51c2cee648b710bf78aeff49cdbe47295aa21021cd43b35", + "storage_service_key": "ccaf84d6a1bae7b5d597c920639f52d3f3c9520f6b33ca8a708ab6e8fdcb7a34", + "svr_key": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "label": "ones", + "logging_key": "de41a80b337d7aeefc49e6d390621f8bea5c8dd19f89a4bf916883ad001136fb", + "registration_lock": "69f195f31069f0786ad2d6b3de4d63ecb020a2c1bbee0ccc2fb1e17197a9ee8c", + "registration_recovery_password": "da181f31c66b99ed0315d9911425aad3ef17b19df10708c02643b67595d56f85", + "storage_service_key": "2de8788431dfc760bd96c5d919efff60b3d3846bc726a6fcaa0a0573795faf06", + "svr_key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "label": "sequence", + "logging_key": "82bf1f24762ff8b1eb0472e949778dd2357d761e966b8ee3b4c6af7996a67630", + "registration_lock": "0c5f865eea5eebe9ef4fa8c4d99cb91de301bebb5b3b91bf775da1a57c725975", + "registration_recovery_password": "bdd982b7baf1d3b4796a2e669befc33b9101bc5eb67f7433dbac815ccb245ae0", + "storage_service_key": "d9fd23ee99c148ad879f62609297139e6a36f415b1ad779d23c687b1fa6d36d9", + "svr_key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" } ] } diff --git a/internal/accountkeysproto/mfa_metadata.pb.go b/internal/accountkeysproto/mfa_metadata.pb.go new file mode 100644 index 000000000..ba23f7eb9 --- /dev/null +++ b/internal/accountkeysproto/mfa_metadata.pb.go @@ -0,0 +1,134 @@ +// Copyright 2026 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: internal/accountkeysproto/mfa_metadata.proto + +package accountkeysproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type MfaKeyMetadataPlaintext struct { + state protoimpl.MessageState `protogen:"open.v1"` + CreationEpochTimeSeconds uint64 `protobuf:"varint,1,opt,name=creation_epoch_time_seconds,json=creationEpochTimeSeconds,proto3" json:"creation_epoch_time_seconds,omitempty"` + KeyName string `protobuf:"bytes,2,opt,name=key_name,json=keyName,proto3" json:"key_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MfaKeyMetadataPlaintext) Reset() { + *x = MfaKeyMetadataPlaintext{} + mi := &file_internal_accountkeysproto_mfa_metadata_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MfaKeyMetadataPlaintext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MfaKeyMetadataPlaintext) ProtoMessage() {} + +func (x *MfaKeyMetadataPlaintext) ProtoReflect() protoreflect.Message { + mi := &file_internal_accountkeysproto_mfa_metadata_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MfaKeyMetadataPlaintext.ProtoReflect.Descriptor instead. +func (*MfaKeyMetadataPlaintext) Descriptor() ([]byte, []int) { + return file_internal_accountkeysproto_mfa_metadata_proto_rawDescGZIP(), []int{0} +} + +func (x *MfaKeyMetadataPlaintext) GetCreationEpochTimeSeconds() uint64 { + if x != nil { + return x.CreationEpochTimeSeconds + } + return 0 +} + +func (x *MfaKeyMetadataPlaintext) GetKeyName() string { + if x != nil { + return x.KeyName + } + return "" +} + +var File_internal_accountkeysproto_mfa_metadata_proto protoreflect.FileDescriptor + +const file_internal_accountkeysproto_mfa_metadata_proto_rawDesc = "" + + "\n" + + ",internal/accountkeysproto/mfa_metadata.proto\x12 signal.account_keys.mfa_metadata\"s\n" + + "\x17MfaKeyMetadataPlaintext\x12=\n" + + "\x1bcreation_epoch_time_seconds\x18\x01 \x01(\x04R\x18creationEpochTimeSeconds\x12\x19\n" + + "\bkey_name\x18\x02 \x01(\tR\akeyNameB?Z=github.com/GoCodeAlone/libsignal-go/internal/accountkeysprotob\x06proto3" + +var ( + file_internal_accountkeysproto_mfa_metadata_proto_rawDescOnce sync.Once + file_internal_accountkeysproto_mfa_metadata_proto_rawDescData []byte +) + +func file_internal_accountkeysproto_mfa_metadata_proto_rawDescGZIP() []byte { + file_internal_accountkeysproto_mfa_metadata_proto_rawDescOnce.Do(func() { + file_internal_accountkeysproto_mfa_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_accountkeysproto_mfa_metadata_proto_rawDesc), len(file_internal_accountkeysproto_mfa_metadata_proto_rawDesc))) + }) + return file_internal_accountkeysproto_mfa_metadata_proto_rawDescData +} + +var file_internal_accountkeysproto_mfa_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_internal_accountkeysproto_mfa_metadata_proto_goTypes = []any{ + (*MfaKeyMetadataPlaintext)(nil), // 0: signal.account_keys.mfa_metadata.MfaKeyMetadataPlaintext +} +var file_internal_accountkeysproto_mfa_metadata_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_internal_accountkeysproto_mfa_metadata_proto_init() } +func file_internal_accountkeysproto_mfa_metadata_proto_init() { + if File_internal_accountkeysproto_mfa_metadata_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_accountkeysproto_mfa_metadata_proto_rawDesc), len(file_internal_accountkeysproto_mfa_metadata_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_internal_accountkeysproto_mfa_metadata_proto_goTypes, + DependencyIndexes: file_internal_accountkeysproto_mfa_metadata_proto_depIdxs, + MessageInfos: file_internal_accountkeysproto_mfa_metadata_proto_msgTypes, + }.Build() + File_internal_accountkeysproto_mfa_metadata_proto = out.File + file_internal_accountkeysproto_mfa_metadata_proto_goTypes = nil + file_internal_accountkeysproto_mfa_metadata_proto_depIdxs = nil +} diff --git a/internal/accountkeysproto/mfa_metadata.proto b/internal/accountkeysproto/mfa_metadata.proto new file mode 100644 index 000000000..0c88a372a --- /dev/null +++ b/internal/accountkeysproto/mfa_metadata.proto @@ -0,0 +1,13 @@ +// Copyright 2026 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only + +syntax = "proto3"; + +package signal.account_keys.mfa_metadata; + +option go_package = "github.com/GoCodeAlone/libsignal-go/internal/accountkeysproto"; + +message MfaKeyMetadataPlaintext { + uint64 creation_epoch_time_seconds = 1; + string key_name = 2; +} diff --git a/internal/upstream/manifest.json b/internal/upstream/manifest.json index 97cf5edaf..af61c5e32 100644 --- a/internal/upstream/manifest.json +++ b/internal/upstream/manifest.json @@ -8,9 +8,9 @@ "report_path": "compat/coverage_manifest.json", "coverage_status": "vector-backed", "checksum_path": "compat/vectors/account-keys.json", - "checksum_sha256": "539d5ae3f25007aea4430c84dbb4c93a23af2c9bf5e654a5d5a0569f012e5b3d", + "checksum_sha256": "995ae42b4adcf30034cc11028c36ebd30a66399a14ee186366e702050082b36b", "official_app_interop_claim": false, - "notes": "Account entropy, SVR key, backup key, backup ID, and PIN access key derivations are locked by committed upstream-compatible vectors." + "notes": "Legacy v0.96.4 account entropy, backup ID, and PIN access key vectors are preserved. The same fixture adds v0.104.0 SVR derivations, PIN master-key SIV, media/thumbnail key splits, and fixed-size MFA metadata; its _note records the immutable upstream commit and tag object. The global compatibility baseline remains v0.96.4 pending the full refresh." }, { "name": "usernames", diff --git a/scripts/update-upstream-pin.sh b/scripts/update-upstream-pin.sh index 87fb0cb87..a03aae546 100755 --- a/scripts/update-upstream-pin.sh +++ b/scripts/update-upstream-pin.sh @@ -36,6 +36,17 @@ esac current_tag="$( sed -nE '/^libsignal-protocol = /s/.*tag = "([^"]+)".*/\1/p' compat/rust-harness/Cargo.toml )" +current_rev="" + +if [ -z "$current_tag" ]; then + metadata="$(cd compat/rust-harness && cargo metadata --format-version 1 --no-deps)" + current_tag="$(jq -r '.packages[] | select(.name == "rust-harness") | .metadata.libsignal.upstream_tag // empty' <<< "$metadata")" + current_rev="$(jq -r '.packages[] | select(.name == "rust-harness") | .dependencies[] | select(.name == "libsignal-protocol") | .source | split("?rev=")[1] // empty' <<< "$metadata")" + if ! [[ "$current_rev" =~ ^[0-9a-f]{40}$ ]]; then + echo "could not find current immutable compat harness revision" >&2 + exit 1 + fi +fi if [ -z "$current_tag" ]; then echo "could not find current compat harness tag" >&2 @@ -52,6 +63,20 @@ echo "updating compat harness from $current_tag to $requested_tag" export CURRENT_TAG="$current_tag" export UPSTREAM_TAG="$requested_tag" +if [ -n "$current_rev" ]; then + tag_object="$(gh api "repos/$upstream_repo/git/ref/tags/$requested_tag" --jq '.object')" + if [ "$(jq -r .type <<< "$tag_object")" = "tag" ]; then + tag_object="$(gh api "repos/$upstream_repo/git/tags/$(jq -r .sha <<< "$tag_object")" --jq '.object')" + fi + requested_rev="$(jq -r .sha <<< "$tag_object")" + if [ "$(jq -r .type <<< "$tag_object")" != "commit" ] || ! [[ "$requested_rev" =~ ^[0-9a-f]{40}$ ]]; then + echo "upstream release tag did not resolve to an immutable commit" >&2 + exit 1 + fi + export CURRENT_REV="$current_rev" UPSTREAM_REV="$requested_rev" + perl -0pi -e 's/\Q$ENV{CURRENT_REV}\E/$ENV{UPSTREAM_REV}/g' compat/rust-harness/Cargo.toml +fi + perl -0pi -e 's/\Q$ENV{CURRENT_TAG}\E/$ENV{UPSTREAM_TAG}/g' \ README.md \ compat/README.md \