Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions abe/cpabe/tkn20/internal/tkn/tk.go
Comment thread
cjpatton marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,17 @@ func decapsulate(header *ciphertextHeader, key *AttributesKey) (*pairing.Gt, err
// We use pi to determine which D to sum into
pi := header.p.pi()
d := max(pi) + 1
Comment thread
cjpatton marked this conversation as resolved.

if len(header.c3) < len(header.p.Inputs) {
return nil, fmt.Errorf("invalid ciphertext: c3 length %d shorter than %d policy wires", len(header.c3), len(header.p.Inputs))
}
if len(header.c3neg) < len(header.p.Inputs) {
return nil, fmt.Errorf("invalid ciphertext: c3neg length %d shorter than %d policy wires", len(header.c3neg), len(header.p.Inputs))
}
if len(header.c2) < d {
return nil, fmt.Errorf("invalid ciphertext: c2 length %d shorter than required %d", len(header.c2), d)
}

// p1, p2 are the left halves of the pairings.
p1 := make([]*matrixG1, d)
p2 := make([]*matrixG1, d)
Expand Down Expand Up @@ -761,6 +772,13 @@ func decapsulate(header *ciphertextHeader, key *AttributesKey) (*pairing.Gt, err
p2[j].add(p2[j], key.k3[mt.label])
}
} else {
// c3neg is required for negative wires but is left nil by
// unmarshalBinary when its serialized entry is empty. Reject such a
// ciphertext instead of dereferencing nil at header.c3neg[mt.wire]
// below (this runs before the MAC check in DecryptCCA).
if header.c3neg[mt.wire] == nil {
return nil, fmt.Errorf("invalid ciphertext: missing c3neg data for negative wire %d", mt.wire)
}
keymat := newMatrixG1(0, 0)
y := &pairing.Scalar{}

Expand Down Expand Up @@ -801,6 +819,13 @@ func decapsulate(header *ciphertextHeader, key *AttributesKey) (*pairing.Gt, err
pairs.addDuals(p2[i], header.c2[i], 1)
}
}
// pTot is nil only when the satisfaction loop matched no wire (e.g. an empty
// policy). Satisfaction already returns an error in that case above, so this
// is a defensive guard: pairAccum.addDuals dereferences its first argument,
// so reaching it with a nil pTot would panic instead of failing cleanly.
if pTot == nil {
return nil, fmt.Errorf("invalid ciphertext: no satisfying policy wires")
}
pairs.addDuals(pTot, key.k1, -1)
pairs.addDuals(key.k2.copy(), header.c1, 1)

Expand Down
174 changes: 174 additions & 0 deletions abe/cpabe/tkn20/internal/tkn/tk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,177 @@ func TestEqualAttributesKey(t *testing.T) {
t.Fatalf("shouldnt be equal")
}
}

// buildSatisfiedHeaderAndKey produces a ciphertext header and a key whose
// attributes satisfy the policy, mirroring the state decapsulate is invoked in
// (header.p is the BK-transformed policy, and c2/c3/c3neg are sized to it).
func buildSatisfiedHeaderAndKey(t *testing.T) (*ciphertextHeader, *AttributesKey) {
t.Helper()
pp, sp, err := GenerateParams(rand.Reader)
if err != nil {
t.Fatal(err)
}
hashKey := []byte("decapsulate-bounds-test")
policy := &Policy{
Inputs: []Wire{
{Label: "country", RawValue: "US", Value: HashStringToScalar(hashKey, "US"), Positive: true},
{Label: "top", RawValue: "secret", Value: HashStringToScalar(hashKey, "secret"), Positive: true},
},
F: Formula{Gates: []Gate{{In0: 0, In1: 1, Out: 2, Class: Andgate}}},
}
attrs := Attributes{
"country": {Value: HashStringToScalar(hashKey, "US")},
"top": {Value: HashStringToScalar(hashKey, "secret")},
}
key, err := DeriveAttributeKeysCCA(rand.Reader, sp, &attrs)
if err != nil {
t.Fatal(err)
}
// EncryptCCA encapsulates with the BK-transformed policy, so do the same to
// obtain a header in the state decapsulate sees after DecryptCCA's transform.
header, _, err := encapsulate(rand.Reader, pp, policy.transformBK(ToScalar(123)))
if err != nil {
t.Fatal(err)
}
return header, key
}

// TestDecapsulateRejectsShortArrays guards against out-of-range access in
// decapsulate: a structurally complete header that declares fewer c2/c3/c3neg
// entries than the policy requires must make decapsulate return an error rather
// than panic with an out-of-range index.
func TestDecapsulateRejectsShortArrays(t *testing.T) {
// Sanity check: the untampered header decapsulates without error.
header, key := buildSatisfiedHeaderAndKey(t)
if _, err := decapsulate(header, key); err != nil {
t.Fatalf("baseline decapsulate failed: %v", err)
}

t.Run("short c3", func(t *testing.T) {
header, key := buildSatisfiedHeaderAndKey(t)
header.c3 = header.c3[:len(header.c3)-1]
assertDecapsulateErrors(t, header, key)
})

t.Run("short c3neg", func(t *testing.T) {
header, key := buildSatisfiedHeaderAndKey(t)
header.c3neg = header.c3neg[:len(header.c3neg)-1]
assertDecapsulateErrors(t, header, key)
})

t.Run("empty c2", func(t *testing.T) {
header, key := buildSatisfiedHeaderAndKey(t)
header.c2 = nil
assertDecapsulateErrors(t, header, key)
})
}

func assertDecapsulateErrors(t *testing.T, header *ciphertextHeader, key *AttributesKey) {
t.Helper()
defer func() {
if r := recover(); r != nil {
t.Fatalf("decapsulate panicked instead of returning an error: %v", r)
}
}()
if _, err := decapsulate(header, key); err == nil {
t.Fatal("decapsulate accepted an inconsistent header without error")
}
}

// TestDecapsulateDegeneratePolicyNoPanic checks that decapsulate fails
// gracefully (rather than panicking) when no policy wire contributes to the
// pairing -- e.g. an empty policy, or a key that does not satisfy the policy.
// Satisfaction rejects these inputs with an error, and decapsulate additionally
// guards the resulting nil accumulator before it reaches pairAccum.addDuals.
func TestDecapsulateDegeneratePolicyNoPanic(t *testing.T) {
pp, sp, err := GenerateParams(rand.Reader)
if err != nil {
t.Fatal(err)
}

t.Run("empty policy", func(t *testing.T) {
attrs := Attributes{}
key, err := DeriveAttributeKeysCCA(rand.Reader, sp, &attrs)
if err != nil {
t.Fatal(err)
}
header, _, err := encapsulate(rand.Reader, pp, &Policy{Inputs: []Wire{}, F: Formula{Gates: []Gate{}}})
if err != nil {
t.Fatal(err)
}
assertDecapsulateErrors(t, header, key)
})

t.Run("non-satisfying key", func(t *testing.T) {
hashKey := []byte("degenerate")
policy := &Policy{
Inputs: []Wire{
{Label: "country", RawValue: "US", Value: HashStringToScalar(hashKey, "US"), Positive: true},
},
F: Formula{Gates: []Gate{}},
}
attrs := Attributes{"other": {Value: HashStringToScalar(hashKey, "nope")}}
key, err := DeriveAttributeKeysCCA(rand.Reader, sp, &attrs)
if err != nil {
t.Fatal(err)
}
header, _, err := encapsulate(rand.Reader, pp, policy.transformBK(ToScalar(7)))
if err != nil {
t.Fatal(err)
}
assertDecapsulateErrors(t, header, key)
})
}

// TestDecapsulateRejectsNilC3neg guards against a nil-pointer dereference in the
// negative-wire path of decapsulate. unmarshalBinary leaves c3neg[i] nil when
// its serialized entry is empty, which is legitimate for positive wires but not
// for negative ones. A malicious ciphertext can mark a wire negative while
// leaving its c3neg empty; if a satisfying key matches that wire, decapsulate
// must return an error rather than dereferencing nil at header.c3neg[mt.wire]
// (which runs before the MAC check in DecryptCCA).
func TestDecapsulateRejectsNilC3neg(t *testing.T) {
pp, sp, err := GenerateParams(rand.Reader)
if err != nil {
t.Fatal(err)
}
hashKey := []byte("nil-c3neg")
// A single negative wire on "country".
policy := &Policy{
Inputs: []Wire{
{Label: "country", RawValue: "US", Value: HashStringToScalar(hashKey, "US"), Positive: false},
},
F: Formula{Gates: []Gate{}},
}
// The key has "country" with a different value, so the negative wire matches.
attrs := Attributes{"country": {Value: HashStringToScalar(hashKey, "CA")}}
key, err := DeriveAttributeKeysCCA(rand.Reader, sp, &attrs)
if err != nil {
t.Fatal(err)
}
header, _, err := encapsulate(rand.Reader, pp, policy.transformBK(ToScalar(99)))
if err != nil {
t.Fatal(err)
}
if header.c3neg[0] == nil {
t.Fatal("expected c3neg[0] to be set for the negative wire")
}

// Drop the negative wire's c3neg entry and round-trip through the wire
// format, mimicking an attacker-serialized header with an empty c3neg entry;
// unmarshalBinary leaves c3neg[0] nil.
header.c3neg[0] = nil
raw, err := header.marshalBinary()
if err != nil {
t.Fatal(err)
}
tampered := &ciphertextHeader{}
if err := tampered.unmarshalBinary(raw); err != nil {
t.Fatal(err)
}
if tampered.c3neg[0] != nil {
t.Fatal("expected unmarshalBinary to leave c3neg[0] nil")
}

assertDecapsulateErrors(t, tampered, key)
}
105 changes: 105 additions & 0 deletions abe/cpabe/tkn20/tkn20_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,108 @@ func TestCouldDecryptPanicsOnOversizedWireList(t *testing.T) {
t.Fatal("malformed ciphertext should not be decryptable")
}
}

// truncateCiphertextHeader takes a legitimate v1.3.8 ciphertext and surgically
// truncates the inner C1 header right after the (structurally valid) policy and
// c1 fields, dropping the c2 count and everything after it. The outer envelope
// is reassembled with corrected length prefixes. Such a header leaves zero bytes
// where ciphertextHeader.unmarshalBinary reads the c2 element count, which must
// be rejected with an error rather than an out-of-range panic.
func truncateCiphertextHeader(t *testing.T, ct []byte) []byte {
t.Helper()

// Ciphertext layout (v1.3.8):
// "v1.3.8" || len16(id) || len32(macData) || len16(tag)
// macData = len32(C1) || len32(env)
// C1 = len16(policy) || len16(c1) || u16(c2Len) || ... || u16(c3Len) || ...
const version = "v1.3.8"
rest := ct[len(version):]
idLen := int(binary.LittleEndian.Uint16(rest))
id := rest[2 : 2+idLen]
rest = rest[2+idLen:]
macLen := int(binary.LittleEndian.Uint32(rest))
macData := rest[4 : 4+macLen]
rest = rest[4+macLen:]
tagLen := int(binary.LittleEndian.Uint16(rest))
tag := rest[2 : 2+tagLen]

c1Len := int(binary.LittleEndian.Uint32(macData))
C1 := macData[4 : 4+c1Len]
envWithPrefix := macData[4+c1Len:]

// Truncate C1 right after the (still valid) policy and c1 fields,
// dropping the c2 count and everything after it.
pLen := int(binary.LittleEndian.Uint16(C1))
off := 2 + pLen
c1bLen := int(binary.LittleEndian.Uint16(C1[off:]))
off += 2 + c1bLen
truncC1 := C1[:off]

// Reassemble the outer ciphertext around the truncated header.
newMac := make([]byte, 4, 4+len(truncC1)+len(envWithPrefix))
binary.LittleEndian.PutUint32(newMac, uint32(len(truncC1)))
newMac = append(newMac, truncC1...)
newMac = append(newMac, envWithPrefix...)

evil := append([]byte{}, []byte(version)...)
evil = append(evil, 0, 0)
binary.LittleEndian.PutUint16(evil[len(evil)-2:], uint16(len(id)))
evil = append(evil, id...)
evil = append(evil, 0, 0, 0, 0)
binary.LittleEndian.PutUint32(evil[len(evil)-4:], uint32(len(newMac)))
evil = append(evil, newMac...)
evil = append(evil, 0, 0)
binary.LittleEndian.PutUint16(evil[len(evil)-2:], uint16(len(tag)))
evil = append(evil, tag...)
return evil
}

// TestTruncatedCiphertextHeaderPanic guards against a denial-of-service in
// ciphertextHeader.unmarshalBinary: a ciphertext truncated right after the
// policy and c1 fields must fail gracefully (returning an error / false) on all
// three public entrypoints rather than panicking.
func TestTruncatedCiphertextHeaderPanic(t *testing.T) {
pk, msk, err := Setup(rand.Reader)
if err != nil {
t.Fatal(err)
}
policy := Policy{}
if err = policy.FromString("(country: US) and (top: secret)"); err != nil {
t.Fatal(err)
}
ct, err := pk.Encrypt(rand.Reader, policy, []byte("hello world"))
if err != nil {
t.Fatal(err)
}

attrs := Attributes{}
attrs.FromMap(map[string]string{"country": "US", "top": "secret"})
key, err := msk.KeyGen(rand.Reader, attrs)
if err != nil {
t.Fatal(err)
}

evil := truncateCiphertextHeader(t, ct)

defer func() {
if r := recover(); r != nil {
t.Fatalf("truncated ciphertext header panicked instead of returning an error: %v", r)
}
}()

// Policy.ExtractFromCiphertext must return an error, not panic.
extracted := &Policy{}
if err := extracted.ExtractFromCiphertext(evil); err == nil {
t.Error("ExtractFromCiphertext accepted a malformed ciphertext")
}

// Attributes.CouldDecrypt must return false, not panic.
if attrs.CouldDecrypt(evil) {
t.Error("CouldDecrypt accepted a malformed ciphertext")
}

// AttributeKey.Decrypt must return an error, not panic.
if _, err := key.Decrypt(evil); err == nil {
t.Error("Decrypt accepted a malformed ciphertext")
}
}
Loading