package sike import ( "crypto/subtle" "errors" "github.com/henrydcase/nobs/hash/shake" "io" ) const ( // n can is max 256-bit (see 1.4 of [SIKE]) MaxMsgLen = 32 MaxSharedSecretSize = 188 MaxSecretByteLenA = 47 ) func cpick(pick int, out, in1, in2 []byte) { var which = byte((int8(pick << 7)) >> 7) for i, _ := range out { out[i] = (in1[i] & which) | (in2[i] & ^which) } } type KEM struct { allocated bool rng io.Reader msg []byte secretBytes []byte } // Zeroize Fp2 func zeroize(fp *Fp2) { // Zeroizing in 2 seperated loops tells compiler to // use fast runtime.memclr() for i := range fp.A { fp.A[i] = 0 } for i := range fp.B { fp.B[i] = 0 } } // Convert the input to wire format. // // The output byte slice must be at least 2*bytelen(p) bytes long. func convFp2ToBytes(output []byte, fp2 *Fp2) { if len(output) < Params.SharedSecretSize { panic("output byte slice too short") } var a Fp2 fromMontDomain(fp2, &a) // convert to bytes in little endian form for i := 0; i < Params.Bytelen; i++ { // set i = j*8 + k tmp := i / 8 k := uint64(i % 8) output[i] = byte(a.A[tmp] >> (8 * k)) output[i+Params.Bytelen] = byte(a.B[tmp] >> (8 * k)) } } // Read 2*bytelen(p) bytes into the given ExtensionFieldElement. // // It is an error to call this function if the input byte slice is less than 2*bytelen(p) bytes long. func convBytesToFp2(fp2 *Fp2, input []byte) { if len(input) < Params.SharedSecretSize { panic("input byte slice too short") } for i := 0; i < Params.Bytelen; i++ { j := i / 8 k := uint64(i % 8) fp2.A[j] |= uint64(input[i]) << (8 * k) fp2.B[j] |= uint64(input[i+Params.Bytelen]) << (8 * k) } toMontDomain(fp2) } // ----------------------------------------------------------------------------- // Functions for traversing isogeny trees acoording to strategy. Key type 'A' is // // Traverses isogeny tree in order to compute xR, xP, xQ and xQmP needed // for public key generation. func traverseTreePublicKeyA(curve *ProjectiveCurveParameters, xR, phiP, phiQ, phiR *ProjectivePoint, pub *PublicKey) { var points = make([]ProjectivePoint, 0, 8) var indices = make([]int, 0, 8) var i, sIdx int var phi isogeny4 cparam := ToEquiv4(curve) strat := pub.params.A.IsogenyStrategy stratSz := len(strat) for j := 1; j <= stratSz; j++ { for i <= stratSz-j { points = append(points, *xR) indices = append(indices, i) k := strat[sIdx] sIdx++ Pow2k(xR, &cparam, 2*k) i += int(k) } cparam = phi.GenerateCurve(xR) for k := 0; k < len(points); k++ { points[k] = phi.EvaluatePoint(&points[k]) } *phiP = phi.EvaluatePoint(phiP) *phiQ = phi.EvaluatePoint(phiQ) *phiR = phi.EvaluatePoint(phiR) // pop xR from points *xR, points = points[len(points)-1], points[:len(points)-1] i, indices = int(indices[len(indices)-1]), indices[:len(indices)-1] } } // Traverses isogeny tree in order to compute xR needed // for public key generation. func traverseTreeSharedKeyA(curve *ProjectiveCurveParameters, xR *ProjectivePoint, pub *PublicKey) { var points = make([]ProjectivePoint, 0, 8) var indices = make([]int, 0, 8) var i, sIdx int var phi isogeny4 cparam := ToEquiv4(curve) strat := pub.params.A.IsogenyStrategy stratSz := len(strat) for j := 1; j <= stratSz; j++ { for i <= stratSz-j { points = append(points, *xR) indices = append(indices, i) k := strat[sIdx] sIdx++ Pow2k(xR, &cparam, 2*k) i += int(k) } cparam = phi.GenerateCurve(xR) for k := 0; k < len(points); k++ { points[k] = phi.EvaluatePoint(&points[k]) } // pop xR from points *xR, points = points[len(points)-1], points[:len(points)-1] i, indices = int(indices[len(indices)-1]), indices[:len(indices)-1] } } // Traverses isogeny tree in order to compute xR, xP, xQ and xQmP needed // for public key generation. func traverseTreePublicKeyB(curve *ProjectiveCurveParameters, xR, phiP, phiQ, phiR *ProjectivePoint, pub *PublicKey) { var points = make([]ProjectivePoint, 0, 8) var indices = make([]int, 0, 8) var i, sIdx int var phi isogeny3 cparam := ToEquiv3(curve) strat := pub.params.B.IsogenyStrategy stratSz := len(strat) for j := 1; j <= stratSz; j++ { for i <= stratSz-j { points = append(points, *xR) indices = append(indices, i) k := strat[sIdx] sIdx++ Pow3k(xR, &cparam, k) i += int(k) } cparam = phi.GenerateCurve(xR) for k := 0; k < len(points); k++ { points[k] = phi.EvaluatePoint(&points[k]) } *phiP = phi.EvaluatePoint(phiP) *phiQ = phi.EvaluatePoint(phiQ) *phiR = phi.EvaluatePoint(phiR) // pop xR from points *xR, points = points[len(points)-1], points[:len(points)-1] i, indices = int(indices[len(indices)-1]), indices[:len(indices)-1] } } // Traverses isogeny tree in order to compute xR, xP, xQ and xQmP needed // for public key generation. func traverseTreeSharedKeyB(curve *ProjectiveCurveParameters, xR *ProjectivePoint, pub *PublicKey) { var points = make([]ProjectivePoint, 0, 8) var indices = make([]int, 0, 8) var i, sIdx int var phi isogeny3 cparam := ToEquiv3(curve) strat := pub.params.B.IsogenyStrategy stratSz := len(strat) for j := 1; j <= stratSz; j++ { for i <= stratSz-j { points = append(points, *xR) indices = append(indices, i) k := strat[sIdx] sIdx++ Pow3k(xR, &cparam, k) i += int(k) } cparam = phi.GenerateCurve(xR) for k := 0; k < len(points); k++ { points[k] = phi.EvaluatePoint(&points[k]) } // pop xR from points *xR, points = points[len(points)-1], points[:len(points)-1] i, indices = int(indices[len(indices)-1]), indices[:len(indices)-1] } } // Generate a public key in the 2-torsion group func publicKeyGenA(pub *PublicKey, prv *PrivateKey) { var xPA, xQA, xRA ProjectivePoint var xPB, xQB, xRB, xK ProjectivePoint var invZP, invZQ, invZR Fp2 var phi isogeny4 // Load points for A xPA = ProjectivePoint{X: prv.params.A.Affine_P, Z: prv.params.OneFp2} xQA = ProjectivePoint{X: prv.params.A.Affine_Q, Z: prv.params.OneFp2} xRA = ProjectivePoint{X: prv.params.A.Affine_R, Z: prv.params.OneFp2} // Load points for B xRB = ProjectivePoint{X: prv.params.B.Affine_R, Z: prv.params.OneFp2} xQB = ProjectivePoint{X: prv.params.B.Affine_Q, Z: prv.params.OneFp2} xPB = ProjectivePoint{X: prv.params.B.Affine_P, Z: prv.params.OneFp2} // Find isogeny kernel xK = ScalarMul3Pt(&pub.params.InitCurve, &xPA, &xQA, &xRA, prv.params.A.SecretBitLen, prv.Scalar) traverseTreePublicKeyA(&pub.params.InitCurve, &xK, &xPB, &xQB, &xRB, pub) // Secret isogeny phi.GenerateCurve(&xK) xPA = phi.EvaluatePoint(&xPB) xQA = phi.EvaluatePoint(&xQB) xRA = phi.EvaluatePoint(&xRB) Fp2Batch3Inv(&xPA.Z, &xQA.Z, &xRA.Z, &invZP, &invZQ, &invZR) mul(&pub.affine_xP, &xPA.X, &invZP) mul(&pub.affine_xQ, &xQA.X, &invZQ) mul(&pub.affine_xQmP, &xRA.X, &invZR) } // Generate a public key in the 3-torsion group func publicKeyGenB(pub *PublicKey, prv *PrivateKey) { var xPB, xQB, xRB, xK ProjectivePoint var xPA, xQA, xRA ProjectivePoint var invZP, invZQ, invZR Fp2 var phi isogeny3 // Load points for A xPA = ProjectivePoint{X: prv.params.A.Affine_P, Z: prv.params.OneFp2} xQA = ProjectivePoint{X: prv.params.A.Affine_Q, Z: prv.params.OneFp2} xRA = ProjectivePoint{X: prv.params.A.Affine_R, Z: prv.params.OneFp2} // Load points for B xRB = ProjectivePoint{X: prv.params.B.Affine_R, Z: prv.params.OneFp2} xQB = ProjectivePoint{X: prv.params.B.Affine_Q, Z: prv.params.OneFp2} xPB = ProjectivePoint{X: prv.params.B.Affine_P, Z: prv.params.OneFp2} xK = ScalarMul3Pt(&pub.params.InitCurve, &xPB, &xQB, &xRB, prv.params.B.SecretBitLen, prv.Scalar) traverseTreePublicKeyB(&pub.params.InitCurve, &xK, &xPA, &xQA, &xRA, pub) phi.GenerateCurve(&xK) xPB = phi.EvaluatePoint(&xPA) xQB = phi.EvaluatePoint(&xQA) xRB = phi.EvaluatePoint(&xRA) Fp2Batch3Inv(&xPB.Z, &xQB.Z, &xRB.Z, &invZP, &invZQ, &invZR) mul(&pub.affine_xP, &xPB.X, &invZP) mul(&pub.affine_xQ, &xQB.X, &invZQ) mul(&pub.affine_xQmP, &xRB.X, &invZR) } // ----------------------------------------------------------------------------- // Key agreement functions // // Establishing shared keys in in 2-torsion group func deriveSecretA(ss []byte, prv *PrivateKey, pub *PublicKey) { var xP, xQ, xQmP ProjectivePoint var xK ProjectivePoint var phi isogeny4 var jInv Fp2 // Recover curve coefficients cparam := pub.params.InitCurve RecoverCoordinateA(&cparam, &pub.affine_xP, &pub.affine_xQ, &pub.affine_xQmP) // Find kernel of the morphism xP = ProjectivePoint{X: pub.affine_xP, Z: pub.params.OneFp2} xQ = ProjectivePoint{X: pub.affine_xQ, Z: pub.params.OneFp2} xQmP = ProjectivePoint{X: pub.affine_xQmP, Z: pub.params.OneFp2} xK = ScalarMul3Pt(&cparam, &xP, &xQ, &xQmP, pub.params.A.SecretBitLen, prv.Scalar) // Traverse isogeny tree traverseTreeSharedKeyA(&cparam, &xK, pub) // Calculate j-invariant on isogeneus curve c := phi.GenerateCurve(&xK) FromEquiv4(&cparam, &c) Jinvariant(&cparam, &jInv) convFp2ToBytes(ss, &jInv) } // Establishing shared keys in in 3-torsion group func deriveSecretB(ss []byte, prv *PrivateKey, pub *PublicKey) { var xP, xQ, xQmP ProjectivePoint var xK ProjectivePoint var phi isogeny3 var jInv Fp2 // Recover curve coefficients cparam := pub.params.InitCurve RecoverCoordinateA(&cparam, &pub.affine_xP, &pub.affine_xQ, &pub.affine_xQmP) // Find kernel of the morphism xP = ProjectivePoint{X: pub.affine_xP, Z: pub.params.OneFp2} xQ = ProjectivePoint{X: pub.affine_xQ, Z: pub.params.OneFp2} xQmP = ProjectivePoint{X: pub.affine_xQmP, Z: pub.params.OneFp2} xK = ScalarMul3Pt(&cparam, &xP, &xQ, &xQmP, pub.params.B.SecretBitLen, prv.Scalar) // Traverse isogeny tree traverseTreeSharedKeyB(&cparam, &xK, pub) // Calculate j-invariant on isogeneus curve c := phi.GenerateCurve(&xK) FromEquiv3(&cparam, &c) Jinvariant(&cparam, &jInv) convFp2ToBytes(ss, &jInv) } func generateCiphertext(ctext []byte, skA *PrivateKey, pkA, pkB *PublicKey, ptext []byte) error { var n [MaxMsgLen]byte var j [MaxSharedSecretSize]byte var ptextLen = len(ptext) if pkB.keyVariant != KeyVariant_SIKE { return errors.New("wrong key type") } err := DeriveSecret(j[:], skA, pkB) if err != nil { return err } f := shake.NewShake256() f.Write(j[:Params.SharedSecretSize]) f.Read(n[:ptextLen]) for i, _ := range ptext { n[i] ^= ptext[i] } pkA.Export(ctext) copy(ctext[pkA.Size():], n[:ptextLen]) return nil } // NewPrivateKey initializes private key. // Usage of this function guarantees that the object is correctly initialized. func NewPrivateKey(v KeyVariant) *PrivateKey { prv := &PrivateKey{key: key{params: &Params, keyVariant: v}} if (v & KeyVariant_SIDH_A) == KeyVariant_SIDH_A { prv.Scalar = make([]byte, prv.params.A.SecretByteLen) } else { prv.Scalar = make([]byte, prv.params.B.SecretByteLen) } if v == KeyVariant_SIKE { prv.S = make([]byte, prv.params.MsgLen) } return prv } // NewPublicKey initializes public key. // Usage of this function guarantees that the object is correctly initialized. func NewPublicKey(v KeyVariant) *PublicKey { return &PublicKey{key: key{params: &Params, keyVariant: v}} } // Import clears content of the public key currently stored in the structure // and imports key stored in the byte string. Returns error in case byte string // size is wrong. Doesn't perform any validation. func (pub *PublicKey) Import(input []byte) error { if len(input) != pub.Size() { return errors.New("sidh: input to short") } ssSz := pub.params.SharedSecretSize convBytesToFp2(&pub.affine_xP, input[0:ssSz]) convBytesToFp2(&pub.affine_xQ, input[ssSz:2*ssSz]) convBytesToFp2(&pub.affine_xQmP, input[2*ssSz:3*ssSz]) return nil } // Exports currently stored key. In case structure hasn't been filled with key data // returned byte string is filled with zeros. func (pub *PublicKey) Export(out []byte) { ssSz := pub.params.SharedSecretSize convFp2ToBytes(out[0:ssSz], &pub.affine_xP) convFp2ToBytes(out[ssSz:2*ssSz], &pub.affine_xQ) convFp2ToBytes(out[2*ssSz:3*ssSz], &pub.affine_xQmP) } // Size returns size of the public key in bytes func (pub *PublicKey) Size() int { return pub.params.PublicKeySize } // Exports currently stored key. In case structure hasn't been filled with key data // returned byte string is filled with zeros. func (prv *PrivateKey) Export(out []byte) { copy(out, prv.S) copy(out[len(prv.S):], prv.Scalar) } // Size returns size of the private key in bytes func (prv *PrivateKey) Size() int { tmp := len(prv.Scalar) if prv.keyVariant == KeyVariant_SIKE { tmp += int(prv.params.MsgLen) } return tmp } // Import clears content of the private key currently stored in the structure // and imports key from octet string. In case of SIKE, the random value 'S' // must be prepended to the value of actual private key (see SIKE spec for details). // Function doesn't import public key value to PrivateKey object. func (prv *PrivateKey) Import(input []byte) error { if len(input) != prv.Size() { return errors.New("sidh: input to short") } copy(prv.S, input[:len(prv.S)]) copy(prv.Scalar, input[len(prv.S):]) return nil } // Generates random private key for SIDH or SIKE. Generated value is // formed as little-endian integer from key-space <2^(e2-1)..2^e2 - 1> // for KeyVariant_A or <2^(s-1)..2^s - 1>, where s = floor(log_2(3^e3)), // for KeyVariant_B. // // Returns error in case user provided RNG fails. func (prv *PrivateKey) Generate(rand io.Reader) error { var err error var dp *DomainParams if (prv.keyVariant & KeyVariant_SIDH_A) == KeyVariant_SIDH_A { dp = &prv.params.A } else { dp = &prv.params.B } if prv.keyVariant == KeyVariant_SIKE && err == nil { _, err = io.ReadFull(rand, prv.S) } // Private key generation takes advantage of the fact that keyspace for secret // key is (0, 2^x - 1), for some possitivite value of 'x' (see SIKE, 1.3.8). // It means that all bytes in the secret key, but the last one, can take any // value between <0x00,0xFF>. Similarily for the last byte, but generation // needs to chop off some bits, to make sure generated value is an element of // a key-space. _, err = io.ReadFull(rand, prv.Scalar) if err != nil { return err } prv.Scalar[len(prv.Scalar)-1] &= (1 << (dp.SecretBitLen % 8)) - 1 // Make sure scalar is SecretBitLen long. SIKE spec says that key // space starts from 0, but I'm not confortable with having low // value scalars used for private keys. It is still secrure as per // table 5.1 in [SIKE]. prv.Scalar[len(prv.Scalar)-1] |= 1 << ((dp.SecretBitLen % 8) - 1) return err } // Generates public key. // // Constant time. func (prv *PrivateKey) GeneratePublicKey(pub *PublicKey) { if (prv.keyVariant & KeyVariant_SIDH_A) == KeyVariant_SIDH_A { publicKeyGenA(pub, prv) } else { publicKeyGenB(pub, prv) } } // Computes a shared secret which is a j-invariant. Function requires that pub has // different KeyVariant than prv. Length of returned output is 2*ceil(log_2 P)/8), // where P is a prime defining finite field. // // It's important to notice that each keypair must not be used more than once // to calculate shared secret. // // Function may return error. This happens only in case provided input is invalid. // Constant time for properly initialized private and public key. func DeriveSecret(ss []byte, prv *PrivateKey, pub *PublicKey) error { if (pub == nil) || (prv == nil) { return errors.New("sidh: invalid arguments") } if (pub.keyVariant == prv.keyVariant) || (pub.params.Id != prv.params.Id) { return errors.New("sidh: public and private are incompatbile") } if (prv.keyVariant & KeyVariant_SIDH_A) == KeyVariant_SIDH_A { deriveSecretA(ss, prv, pub) } else { deriveSecretB(ss, prv, pub) } return nil } // Uses SIKE public key to encrypt plaintext. Requires cryptographically secure PRNG // Returns ciphertext in case encryption succeeds. Returns error in case PRNG fails // or wrongly formated input was provided. func encrypt(ctext []byte, rng io.Reader, pub *PublicKey, ptext []byte) error { var ptextLen = len(ptext) // c1 must be security level + 64 bits (see [SIKE] 1.4 and 4.3.3) if ptextLen != pub.params.KemSize { return errors.New("Unsupported message length") } skA := NewPrivateKey(KeyVariant_SIDH_A) pkA := NewPublicKey(KeyVariant_SIDH_A) err := skA.Generate(rng) if err != nil { return err } skA.GeneratePublicKey(pkA) return generateCiphertext(ctext, skA, pkA, pub, ptext) } // Uses SIKE private key to decrypt ciphertext. Returns plaintext in case // decryption succeeds or error in case unexptected input was provided. // Constant time func decrypt(n []byte, prv *PrivateKey, ctext []byte) (int, error) { var c1_len int var j [MaxSharedSecretSize]byte var pk_len = prv.params.PublicKeySize if prv.keyVariant != KeyVariant_SIKE { return 0, errors.New("wrong key type") } // ctext is a concatenation of (ciphertext = pubkey_A || c1) // it must be security level + 64 bits (see [SIKE] 1.4 and 4.3.3) c1_len = len(ctext) - pk_len if c1_len != prv.params.KemSize { return 0, errors.New("wrong size of cipher text") } c0 := NewPublicKey(KeyVariant_SIDH_A) err := c0.Import(ctext[:pk_len]) if err != nil { return 0, err } err = DeriveSecret(j[:], prv, c0) if err != nil { return 0, err } s := shake.NewShake256() s.Write(j[:Params.SharedSecretSize]) s.Read(n[:c1_len]) for i, _ := range n[:c1_len] { n[i] ^= ctext[pk_len+i] } return c1_len, nil } // KEM API func (kem *KEM) Allocate(rng io.Reader) { kem.allocated = true kem.rng = rng kem.msg = make([]byte, Params.MsgLen) kem.secretBytes = make([]byte, Params.A.SecretByteLen) } func (kem *KEM) Reset() { for i, _ := range kem.msg { kem.msg[i] = 0 } for i, _ := range kem.secretBytes { kem.secretBytes[i] = 0 } } func (kem *KEM) CiphertextSize() int { return Params.CiphertextSize } func (kem *KEM) SharedSecretSize() int { return Params.KemSize } // Encapsulation receives the public key and generates SIKE ciphertext and shared secret. // The generated ciphertext is used for authentication. // The rng must be cryptographically secure PRNG. // Error is returned in case PRNG fails or wrongly formated input was provided. func (kem *KEM) Encapsulate(ciphertext, secret []byte, pub *PublicKey) error { var buf [3 * MaxSharedSecretSize]byte var skA = PrivateKey{ key: key{ params: &Params, keyVariant: KeyVariant_SIDH_A}, Scalar: kem.secretBytes} if !kem.allocated { panic("KEM unallocated") } // Generate ephemeral value _, err := io.ReadFull(kem.rng, kem.msg[:]) if err != nil { return err } pub.Export(buf[:]) g := shake.NewShake256() g.Write(kem.msg) g.Write(buf[:3*Params.SharedSecretSize]) g.Read(skA.Scalar) // Ensure bitlength is not bigger then to 2^e2-1 skA.Scalar[len(skA.Scalar)-1] &= (1 << (pub.params.A.SecretBitLen % 8)) - 1 pkA := NewPublicKey(KeyVariant_SIDH_A) skA.GeneratePublicKey(pkA) err = generateCiphertext(ciphertext, &skA, pkA, pub, kem.msg[:]) if err != nil { return err } // K = H(msg||(c0||c1)) h := shake.NewShake256() h.Write(kem.msg) h.Write(ciphertext) h.Read(secret) return nil } // Decapsulate given the keypair and ciphertext as inputs, Decapsulate outputs a shared // secret if plaintext verifies correctly, otherwise function outputs random value. // Decapsulation may fail in case input is wrongly formated. // Constant time for properly initialized input. func (kem *KEM) Decapsulate(secret []byte, prv *PrivateKey, pub *PublicKey, ctext []byte) error { var m [MaxMsgLen]byte var r [MaxSecretByteLenA]byte var pkBytes [3 * MaxSharedSecretSize]byte var skA = PrivateKey{ key: key{ params: &Params, keyVariant: KeyVariant_SIDH_A}, Scalar: kem.secretBytes} var pkA = NewPublicKey(KeyVariant_SIDH_A) c1_len, err := decrypt(m[:], prv, ctext) if err != nil { return err } // r' = G(m'||pub) //var key = make([]byte, pub.Size()+2*Params.MsgLen) pub.Export(pkBytes[:]) g := shake.NewShake256() g.Write(m[:c1_len]) g.Write(pkBytes[:3*pub.params.SharedSecretSize]) g.Read(r[:pub.params.A.SecretByteLen]) // Ensure bitlength is not bigger than 2^e2-1 r[pub.params.A.SecretByteLen-1] &= (1 << (pub.params.A.SecretBitLen % 8)) - 1 // Never fails skA.Import(r[:pub.params.A.SecretByteLen]) skA.GeneratePublicKey(pkA) pkA.Export(pkBytes[:]) // S is chosen at random when generating a key and unknown to other party. It // may seem weird, but it's correct. It is important that S is unpredictable // to other party. Without this check, it is possible to recover a secret, by // providing series of invalid ciphertexts. It is also important that isn case // // See more details in "On the security of supersingular isogeny cryptosystems" // (S. Galbraith, et al., 2016, ePrint #859). mask := subtle.ConstantTimeCompare(pkBytes[:pub.params.PublicKeySize], ctext[:pub.params.PublicKeySize]) cpick(mask, m[:c1_len], m[:c1_len], prv.S) h := shake.NewShake256() h.Write(m[:c1_len]) h.Write(ctext) h.Read(secret) return nil }