-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Publisher Advertiser Identity Reconciliation (PAIR) library (#74)
- Loading branch information
Showing
3 changed files
with
217 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Publisher Advertiser Identity Reconciliation (PAIR) | ||
|
||
## Overview | ||
Publisher Advertiser Identity Reconciliation (PAIR) [1] is a privacy-centric protocol that leverages commutative encryptions like the [Diffie-Hellman PSI](https://github.com/Optable/match/blob/main/pkg/dhpsi/README.md) to reconcile the first party identitfiers of the publisher and the advertiser, and allows a secure programmatic activation for the advertiser following a PAIR match. | ||
|
||
This package provides a reference implementation of core utility functions required to run the PAIR protocol. | ||
|
||
## Protocol | ||
The PAIR protocol in the dual clean room scenario involves two clean room operators, one responsible for the publisher and the other for the advertiser. The protocol consists of the following steps: | ||
|
||
### Key generation and management | ||
1. the publisher clean room operator __Pub__ and the advertiser clean room operator __Adv__ agree on a hashing function (SHA256) and a preset elliptic curve (Curve25519). | ||
2. __Pub__ generates a random hash salt _s_, and a private key (*scalar*) _p_, and rotates them periodically. _s_ is rotated every 30 days, and _p_ is rotated every 180 days. | ||
2. __Adv__ generates a private key (*scalar*) _a_, and rotates it every 180 days. | ||
|
||
### offline matching | ||
1. __Pub__ shares the hash salt _s_ with __Adv__. | ||
2. __Pub__ hashes each identifier _x<sub>i</sub> ∈ X_ from his input audience list _X_ and encrypts the hashed identifiers using _p_ to obtain _E<sub>p</sub>(H<sub>s</sub>(x<sub>i</sub>))_, which is also known as the Publisher ID. | ||
3. __Adv__ hashes each identifier _y<sub>i</sub> ∈ Y_ from his input audience list _Y_ and encrypts the hashed identifiers using _a_ to obtain _E<sub>a</sub>(H<sub>s</sub>(y<sub>i</sub>))_, which is also known as the Advertiser ID. | ||
4. __Pub__ and __Adv__ exchange the Publisher IDs and Advertiser IDs respectively. | ||
5. __Pub__ encrypts the Advertiser IDs using _p_ to obtain _E<sub>p</sub>(E<sub>a</sub>(H<sub>s</sub>(y<sub>i</sub>)))_, the doubly encrypted and hashed identifier is known as the PAIR ID. | ||
6. __Adv__ encrypts the Publisher IDs using _a_ to obtain _E<sub>a</sub>(E<sub>p</sub>(H<sub>s</sub>(x<sub>i</sub>)))_, which is known as the PAIR ID. | ||
7. __Pub__ and __Adv__ exchange the PAIR IDs respectively. | ||
8. __Pub__ intersects the PAIR IDs to obtain the match rate, and output a table containing his un-encrypted identitifiers _x<sub>i</sub>_ and its Publisher ID counter part _E<sub>p</sub>(H<sub>s</sub>(x<sub>i</sub>))_. | ||
9. __Adv__ intersects the PAIR IDs to obtain the match rate, and the intersected PAIR IDs. __Adv__ decrypts the PAIR IDs using _a_ to obtain the intersected Publisher IDs _E<sub>p</sub>(H<sub>s</sub>(y<sub>i</sub>))_. | ||
|
||
### online activation | ||
1. __Adv__ sends the intersected Publisher IDs to his Demand Side Platform (DSP) for activation. | ||
2. __Pub__ keeps the mapping of his identifier _x<sub>i</sub>_ and the Publisher ID _E<sub>p</sub>(H<sub>s</sub>(x<sub>i</sub>))_. | ||
3. When a user visits the publisher's website, the publisher looks up the Publisher ID of the visitor and prepares an OpenRTB bid request containing the Publisher ID to his Sell Side Platform (SSP). | ||
4. The SSP sends the bid request to the DSP. | ||
5. The DSP looks up the Publisher ID in the list of intersected Publisher ID sent by __Adv__ and decides the outcome of the bid request. | ||
|
||
## References | ||
[1] TBD. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package pair | ||
|
||
import ( | ||
"crypto" | ||
"crypto/sha512" | ||
"errors" | ||
"hash" | ||
|
||
"github.com/gtank/ristretto255" | ||
) | ||
|
||
type PAIRMode uint8 | ||
|
||
const ( | ||
// PAIRSHA256Ristretto255 is PAIR with SHA256 as hash function and Ristretto255 as the group. | ||
PAIRSHA256Ristretto255 PAIRMode = 0x01 | ||
) | ||
|
||
const ( | ||
sha256SaltSize = 32 | ||
) | ||
|
||
var ( | ||
ErrInvalidPAIRMode = errors.New("invalid PAIR mode") | ||
ErrInvalidSaltSize = errors.New("invalid hash salt size") | ||
) | ||
|
||
// PrivateKey represents a PAIR private key. | ||
type PrivateKey struct { | ||
// h is the hash function used to hash the data | ||
h hash.Hash | ||
|
||
// salt for h | ||
salt []byte | ||
|
||
// private key | ||
scalar *ristretto255.Scalar | ||
} | ||
|
||
// New instantiates a new private key with the given salt and scalar. | ||
// It expects the scalar to be base64 encoded. | ||
func (p PAIRMode) New(salt []byte, scalar []byte) (*PrivateKey, error) { | ||
pk := new(PrivateKey) | ||
|
||
switch p { | ||
case PAIRSHA256Ristretto255: | ||
pk.h = crypto.SHA256.New() | ||
if len(salt) != sha256SaltSize { | ||
return nil, ErrInvalidSaltSize | ||
} | ||
pk.salt = salt | ||
default: | ||
return nil, ErrInvalidPAIRMode | ||
} | ||
|
||
pk.scalar = ristretto255.NewScalar() | ||
if err := pk.scalar.UnmarshalText(scalar); err != nil { | ||
return nil, err | ||
} | ||
|
||
return pk, nil | ||
} | ||
|
||
// hash hashes the data using the private key's hash function with the salt. | ||
func (pk *PrivateKey) hash(data []byte) []byte { | ||
// salt the hash function | ||
pk.h.Write(pk.salt) | ||
// hash the data | ||
pk.h.Write(data) | ||
return pk.h.Sum(nil) | ||
} | ||
|
||
// Encrypt first hashes the data with a salted hash function, | ||
// it then derives the hashed data to an element of the group | ||
// and encrypts it using the private key. | ||
func (pk *PrivateKey) Encrypt(data []byte) ([]byte, error) { | ||
// hash the data | ||
data = pk.hash(data) | ||
|
||
// map hashed data to a point on the curve | ||
element := ristretto255.NewElement() | ||
uniformized := sha512.Sum512(data) | ||
element.FromUniformBytes(uniformized[:]) | ||
|
||
// encrypt the data | ||
element.ScalarMult(pk.scalar, element) | ||
|
||
// return base64 encoded encrypted data | ||
return element.MarshalText() | ||
} | ||
|
||
// ReEncrypt re-encrypts the ciphertext using the same private key. | ||
func (pk *PrivateKey) ReEncrypt(ciphertext []byte) ([]byte, error) { | ||
// unmarshal the ciphertext to an element of the group | ||
cipher := ristretto255.NewElement() | ||
if err := cipher.UnmarshalText(ciphertext); err != nil { | ||
return nil, err | ||
} | ||
|
||
// re-encrypt the group element by multiplying it with the private key | ||
cipher.ScalarMult(pk.scalar, cipher) | ||
|
||
return cipher.MarshalText() | ||
} | ||
|
||
// Decrypt undoes the encryption using the private key once, and returns the element of the group. | ||
func (pk *PrivateKey) Decrypt(ciphertext []byte) ([]byte, error) { | ||
// unmarshal the ciphertext to an element of the group | ||
cipher := ristretto255.NewElement() | ||
if err := cipher.UnmarshalText(ciphertext); err != nil { | ||
return nil, err | ||
} | ||
|
||
// decrypt the group element by multiplying it with the inverse of the private key | ||
inverse := ristretto255.NewScalar() | ||
inverse.Invert(pk.scalar) | ||
|
||
cipher.ScalarMult(inverse, cipher) | ||
|
||
return cipher.MarshalText() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package pair | ||
|
||
import ( | ||
"crypto/rand" | ||
"crypto/sha512" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/gtank/ristretto255" | ||
) | ||
|
||
func TestPAIR(t *testing.T) { | ||
var ( | ||
salt = make([]byte, sha256SaltSize) | ||
scalar = ristretto255.NewScalar() | ||
) | ||
|
||
if _, err := rand.Read(salt); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// sha512 produces a 64-byte psuedo-uniformized data | ||
src := sha512.Sum512(salt) | ||
scalar.FromUniformBytes(src[:]) | ||
sk, err := scalar.MarshalText() | ||
if err != nil { | ||
t.Fatalf("failed to marshal the scalar: %s", err.Error()) | ||
} | ||
|
||
// Create a new PAIR instance | ||
pairID := PAIRSHA256Ristretto255 | ||
|
||
pair, err := pairID.New(salt, sk) | ||
if err != nil { | ||
t.Fatalf("failed to instantiate a new PAIR instance: %s", err.Error()) | ||
} | ||
|
||
var data = []byte("[email protected]") | ||
|
||
// Encrypt the data | ||
ciphertext, err := pair.Encrypt(data) | ||
if err != nil { | ||
t.Fatalf("failed to encrypt the data: %s", err.Error()) | ||
} | ||
|
||
// Re-encrypt the data | ||
ciphertext2, err := pair.ReEncrypt(ciphertext) | ||
if err != nil { | ||
t.Fatalf("failed to re-encrypt the data: %s", err.Error()) | ||
} | ||
|
||
// Decrypt the data | ||
decrypted, err := pair.Decrypt(ciphertext2) | ||
if err != nil { | ||
t.Fatalf("failed to decrypt the data: %s", err.Error()) | ||
} | ||
|
||
if strings.Compare(string(ciphertext), string(decrypted)) != 0 { | ||
t.Fatalf("want: %s, got: %s", string(ciphertext), string(decrypted)) | ||
} | ||
} |