// Copyright (c) 2019, Cloudflare Inc. // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. package sike import ( "crypto/sha256" "crypto/subtle" "errors" "io" ) // Zeroize Fp2 func zeroize(fp *Fp2) { // Zeroizing in 2 separated 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) < 2*Params.Bytelen { 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) < 2*Params.Bytelen { 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 cparam := CalcCurveParamsEquiv4(curve) phi := NewIsogeny4() 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 cparam := CalcCurveParamsEquiv4(curve) phi := NewIsogeny4() 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 cparam := CalcCurveParamsEquiv3(curve) phi := NewIsogeny3() 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 cparam := CalcCurveParamsEquiv3(curve) phi := NewIsogeny3() 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(prv *PrivateKey) (pub *PublicKey) { var xPA, xQA, xRA ProjectivePoint var xPB, xQB, xRB, xK ProjectivePoint var invZP, invZQ, invZR Fp2 pub = NewPublicKey(KeyVariant_SIDH_A) var phi = NewIsogeny4() // 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) return } // Generate a public key in the 3-torsion group func publicKeyGenB(prv *PrivateKey) (pub *PublicKey) { var xPB, xQB, xRB, xK ProjectivePoint var xPA, xQA, xRA ProjectivePoint var invZP, invZQ, invZR Fp2 pub = NewPublicKey(prv.keyVariant) var phi = NewIsogeny3() // 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} // 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} 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) return } // ----------------------------------------------------------------------------- // Key agreement functions // // Establishing shared keys in in 2-torsion group func deriveSecretA(prv *PrivateKey, pub *PublicKey) []byte { var sharedSecret = make([]byte, pub.params.SharedSecretSize) var xP, xQ, xQmP ProjectivePoint var xK ProjectivePoint var cparam ProjectiveCurveParameters var phi = NewIsogeny4() var jInv Fp2 // Recover curve coefficients RecoverCoordinateA(&cparam, &pub.affine_xP, &pub.affine_xQ, &pub.affine_xQmP) // C=1 cparam.C = Params.OneFp2 // 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) RecoverCurveCoefficients4(&cparam, &c) Jinvariant(&cparam, &jInv) convFp2ToBytes(sharedSecret, &jInv) return sharedSecret } // Establishing shared keys in in 3-torsion group func deriveSecretB(prv *PrivateKey, pub *PublicKey) []byte { var sharedSecret = make([]byte, pub.params.SharedSecretSize) var xP, xQ, xQmP ProjectivePoint var xK ProjectivePoint var cparam ProjectiveCurveParameters var phi = NewIsogeny3() var jInv Fp2 // Recover curve A coefficient RecoverCoordinateA(&cparam, &pub.affine_xP, &pub.affine_xQ, &pub.affine_xQmP) // C=1 cparam.C = Params.OneFp2 // 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) RecoverCurveCoefficients3(&cparam, &c) Jinvariant(&cparam, &jInv) convFp2ToBytes(sharedSecret, &jInv) return sharedSecret } func encrypt(skA *PrivateKey, pkA, pkB *PublicKey, ptext []byte) ([]byte, error) { if pkB.keyVariant != KeyVariant_SIKE { return nil, errors.New("wrong key type") } j, err := DeriveSecret(skA, pkB) if err != nil { return nil, err } if len(ptext) != pkA.params.KemSize { panic("Implementation error") } digest := sha256.Sum256(j) // Uses truncated digest (first 16-bytes) for i, _ := range ptext { digest[i] ^= ptext[i] } ret := make([]byte, pkA.Size()+len(ptext)) copy(ret, pkA.Export()) copy(ret[pkA.Size():], digest[:pkA.params.KemSize]) return ret, 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() []byte { output := make([]byte, pub.params.PublicKeySize) ssSz := pub.params.SharedSecretSize convFp2ToBytes(output[0:ssSz], &pub.affine_xP) convFp2ToBytes(output[ssSz:2*ssSz], &pub.affine_xQ) convFp2ToBytes(output[2*ssSz:3*ssSz], &pub.affine_xQmP) return output } // 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() []byte { ret := make([]byte, len(prv.Scalar)+len(prv.S)) copy(ret, prv.S) copy(ret[len(prv.S):], prv.Scalar) return ret } // 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 = 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() *PublicKey { if (prv.keyVariant & KeyVariant_SIDH_A) == KeyVariant_SIDH_A { return publicKeyGenA(prv) } return publicKeyGenB(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(prv *PrivateKey, pub *PublicKey) ([]byte, error) { if (pub == nil) || (prv == nil) { return nil, errors.New("sidh: invalid arguments") } if (pub.keyVariant == prv.keyVariant) || (pub.params.Id != prv.params.Id) { return nil, errors.New("sidh: public and private are incompatbile") } if (prv.keyVariant & KeyVariant_SIDH_A) == KeyVariant_SIDH_A { return deriveSecretA(prv, pub), nil } else { return deriveSecretB(prv, pub), 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 formatted input was provided. func Encrypt(rng io.Reader, pub *PublicKey, ptext []byte) ([]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 nil, errors.New("Unsupported message length") } skA := NewPrivateKey(KeyVariant_SIDH_A) err := skA.Generate(rng) if err != nil { return nil, err } pkA := skA.GeneratePublicKey() return encrypt(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(prv *PrivateKey, ctext []byte) ([]byte, error) { var c1_len int n := make([]byte, prv.params.KemSize) pk_len := prv.params.PublicKeySize if prv.keyVariant != KeyVariant_SIKE { return nil, errors.New("wrong key type") } // ctext is a concatenation of (pubkey_A || c1=ciphertext) // it must be security level + 64 bits (see [SIKE] 1.4 and 4.3.3) c1_len = len(ctext) - pk_len if c1_len != int(prv.params.KemSize) { return nil, errors.New("wrong size of cipher text") } c0 := NewPublicKey(KeyVariant_SIDH_A) err := c0.Import(ctext[:pk_len]) if err != nil { return nil, err } j, err := DeriveSecret(prv, c0) if err != nil { return nil, err } digest := sha256.Sum256(j) copy(n, digest[:]) for i, _ := range n { n[i] ^= ctext[pk_len+i] } return n[:c1_len], nil } // 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 formatted input was provided. func Encapsulate(rng io.Reader, pub *PublicKey) (ctext []byte, secret []byte, err error) { // Buffer for random, secret message ptext := make([]byte, pub.params.MsgLen) // SHA256 hash context object d := sha256.New() // Generate ephemeral value _, err = io.ReadFull(rng, ptext) if err != nil { return nil, nil, err } // Implementation uses first 28-bytes of secret d.Write(ptext) d.Write(pub.Export()) digest := d.Sum(nil) // r = G(ptext||pub) r := digest[:pub.params.A.SecretByteLen] // (c0 || c1) = Enc(pkA, ptext; r) skA := NewPrivateKey(KeyVariant_SIDH_A) err = skA.Import(r) if err != nil { return nil, nil, err } pkA := skA.GeneratePublicKey() ctext, err = encrypt(skA, pkA, pub, ptext) if err != nil { return nil, nil, err } // K = H(ptext||(c0||c1)) d.Reset() d.Write(ptext) d.Write(ctext) digest = d.Sum(digest[:0]) return ctext, digest[:pub.params.KemSize], 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 formatted. // Constant time for properly initialized input. func Decapsulate(prv *PrivateKey, pub *PublicKey, ctext []byte) ([]byte, error) { var skA = NewPrivateKey(KeyVariant_SIDH_A) // SHA256 hash context object d := sha256.New() m, err := Decrypt(prv, ctext) if err != nil { return nil, err } // r' = G(m'||pub) d.Write(m) d.Write(pub.Export()) digest := d.Sum(nil) // Never fails skA.Import(digest[:pub.params.A.SecretByteLen]) // Never fails pkA := skA.GeneratePublicKey() c0 := pkA.Export() d.Reset() if subtle.ConstantTimeCompare(c0, ctext[:len(c0)]) == 1 { d.Write(m) } else { // S is chosen at random when generating a key and is unknown to the 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 in case // // See more details in "On the security of supersingular isogeny cryptosystems" // (S. Galbraith, et al., 2016, ePrint #859). d.Write(prv.S) } d.Write(ctext) digest = d.Sum(digest[:0]) return digest[:pub.params.KemSize], nil }