Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve curve25519-based crypto #6050

Closed
wants to merge 18 commits into from
Closed
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
15 changes: 13 additions & 2 deletions lib/std/crypto.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ pub const chaCha20IETF = import_chaCha20.chaCha20IETF;
pub const chaCha20With64BitNonce = import_chaCha20.chaCha20With64BitNonce;

pub const Poly1305 = @import("crypto/poly1305.zig").Poly1305;
pub const X25519 = @import("crypto/x25519.zig").X25519;

const import_aes = @import("crypto/aes.zig");
pub const AES128 = import_aes.AES128;
pub const AES256 = import_aes.AES256;

pub const Curve25519 = @import("crypto/25519/curve25519.zig").Curve25519;
pub const Ed25519 = @import("crypto/25519/ed25519.zig").Ed25519;
pub const Edwards25519 = @import("crypto/25519/edwards25519.zig").Edwards25519;
pub const X25519 = @import("crypto/25519/x25519.zig").X25519;
pub const Ristretto255 = @import("crypto/25519/ristretto255.zig").Ristretto255;

const std = @import("std.zig");
pub const randomBytes = std.os.getrandom;

Expand All @@ -55,7 +60,13 @@ test "crypto" {
_ = @import("crypto/sha1.zig");
_ = @import("crypto/sha2.zig");
_ = @import("crypto/sha3.zig");
_ = @import("crypto/x25519.zig");
_ = @import("crypto/25519/curve25519.zig");
_ = @import("crypto/25519/ed25519.zig");
_ = @import("crypto/25519/edwards25519.zig");
_ = @import("crypto/25519/field.zig");
_ = @import("crypto/25519/scalar.zig");
_ = @import("crypto/25519/x25519.zig");
_ = @import("crypto/25519/ristretto255.zig");
}

test "issue #4532: no index out of bounds" {
Expand Down
144 changes: 144 additions & 0 deletions lib/std/crypto/25519/curve25519.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const std = @import("std");

/// Group operations over Curve25519.
pub const Curve25519 = struct {
/// The underlying prime field.
pub const Fe = @import("field.zig").Fe;
/// Field arithmetic mod the order of the main subgroup.
pub const scalar = @import("scalar.zig");

x: Fe,

/// Decode a Curve25519 point from its compressed (X) coordinates.
pub inline fn fromBytes(s: [32]u8) Curve25519 {
return .{ .x = Fe.fromBytes(s) };
}

/// Encode a Curve25519 point.
pub inline fn toBytes(p: Curve25519) [32]u8 {
return p.x.toBytes();
}

/// The Curve25519 base point.
pub const basePoint = Curve25519{ .x = Fe.curve25519BasePoint };

/// Check that the encoding of a Curve25519 point is canonical.
pub fn rejectNonCanonical(s: [32]u8) !void {
return Fe.rejectNonCanonical(s, false);
}

/// Reject the neutral element.
pub fn rejectIdentity(p: Curve25519) !void {
if (p.x.isZero()) {
return error.IdentityElement;
}
}

fn ladder(p: Curve25519, s: [32]u8, comptime bits: usize) !Curve25519 {
var x1 = p.x;
var x2 = Fe.one;
var z2 = Fe.zero;
var x3 = x1;
var z3 = Fe.one;
var swap: u8 = 0;
var pos: usize = bits - 1;
while (true) : (pos -= 1) {
const bit = (s[pos >> 3] >> @truncate(u3, pos)) & 1;
swap ^= bit;
Fe.cSwap2(&x2, &x3, &z2, &z3, swap);
swap = bit;
const a = x2.add(z2);
const b = x2.sub(z2);
const aa = a.sq();
const bb = b.sq();
x2 = aa.mul(bb);
const e = aa.sub(bb);
const da = x3.sub(z3).mul(a);
const cb = x3.add(z3).mul(b);
x3 = da.add(cb).sq();
z3 = x1.mul(da.sub(cb).sq());
z2 = e.mul(bb.add(e.mul32(121666)));
if (pos == 0) break;
}
Fe.cSwap2(&x2, &x3, &z2, &z3, swap);
z2 = z2.invert();
x2 = x2.mul(z2);
if (x2.isZero()) {
return error.IdentityElement;
}
return Curve25519{ .x = x2 };
}

/// Multiply a Curve25519 point by a scalar after "clamping" it.
/// Clamping forces the scalar to be a multiple of the cofactor in
/// order to prevent small subgroups attacks. This is the standard
/// way to use Curve25519 for a DH operation.
/// Return error.IdentityElement if the resulting point is
/// the identity element.
pub fn clampedMul(p: Curve25519, s: [32]u8) !Curve25519 {
var t: [32]u8 = s;
scalar.clamp(&t);
return try ladder(p, t, 255);
}

/// Multiply a Curve25519 point by a scalar without clamping it.
/// Return error.IdentityElement if the resulting point is
/// the identity element or error.WeakPublicKey if the public
/// key is a low-order point.
pub fn mul(p: Curve25519, s: [32]u8) !Curve25519 {
const cofactor = [_]u8{8} ++ [_]u8{0} ** 31;
_ = ladder(p, cofactor, 4) catch |_| return error.WeakPublicKey;
return try ladder(p, s, 256);
}
};

test "curve25519" {
var s = [32]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 };
const p = try Curve25519.basePoint.clampedMul(s);
try p.rejectIdentity();
var buf: [128]u8 = undefined;
std.testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{p.toBytes()}), "E6F2A4D1C28EE5C7AD0329268255A468AD407D2672824C0C0EB30EA6EF450145");
const q = try p.clampedMul(s);
std.testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{q.toBytes()}), "3614E119FFE55EC55B87D6B19971A9F4CBC78EFE80BEC55B96392BABCC712537");

try Curve25519.rejectNonCanonical(s);
s[31] |= 0x80;
std.testing.expectError(error.NonCanonical, Curve25519.rejectNonCanonical(s));
}

test "curve25519 small order check" {
var s: [32]u8 = [_]u8{1} ++ [_]u8{0} ** 31;
const small_order_ss: [7][32]u8 = .{
.{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0 (order 4)
},
.{
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1 (order 1)
},
.{
0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00, // 325606250916557431795983626356110631294008115727848805560023387167927233504 (order 8) */
},
.{
0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57, // 39382357235489614581723060781553021112529911719440698176882885853963445705823 (order 8)
},
.{
0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, // p-1 (order 2)
},
.{
0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, // p (=0, order 4)
},
.{
0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, // p+1 (=1, order 1)
},
};
for (small_order_ss) |small_order_s| {
std.testing.expectError(error.WeakPublicKey, Curve25519.fromBytes(small_order_s).mul(s));
var extra = small_order_s;
extra[31] ^= 0x80;
std.testing.expectError(error.WeakPublicKey, Curve25519.fromBytes(extra).mul(s));
var valid = small_order_s;
valid[31] = 0x40;
s[0] = 0;
std.testing.expectError(error.IdentityElement, Curve25519.fromBytes(valid).mul(s));
}
}
134 changes: 134 additions & 0 deletions lib/std/crypto/25519/ed25519.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const std = @import("std");
const fmt = std.fmt;
const mem = std.mem;
const Sha512 = std.crypto.Sha512;

/// Ed25519 (EdDSA) signatures.
pub const Ed25519 = struct {
/// The underlying elliptic curve.
pub const Curve = @import("edwards25519.zig").Edwards25519;
/// Length (in bytes) of a seed required to create a key pair.
pub const seed_length = 32;
/// Length (in bytes) of a compressed key pair.
pub const keypair_length = 64;
/// Length (in bytes) of a compressed public key.
pub const public_length = 32;
/// Length (in bytes) of a signature.
pub const signature_length = 64;
/// Length (in bytes) of optional random bytes, for non-deterministic signatures.
pub const noise_length = 32;

/// Derive a key pair from a secret seed.
///
/// As in RFC 8032, an Ed25519 public key is generated by hashing
/// the secret key using the SHA-512 function, and interpreting the
/// bit-swapped, clamped lower-half of the output as the secret scalar.
///
/// For this reason, an EdDSA secret key is commonly called a seed,
/// from which the actual secret is derived.
pub fn createKeyPair(seed: [seed_length]u8) ![keypair_length]u8 {
var az: [Sha512.digest_length]u8 = undefined;
var h = Sha512.init();
h.update(&seed);
h.final(&az);
const p = try Curve.basePoint.clampedMul(az[0..32].*);
var keypair: [keypair_length]u8 = undefined;
mem.copy(u8, &keypair, &seed);
mem.copy(u8, keypair[seed_length..], &p.toBytes());
return keypair;
}

/// Return the public key for a given key pair.
pub fn publicKey(key_pair: [keypair_length]u8) [public_length]u8 {
var public_key: [public_length]u8 = undefined;
mem.copy(u8, public_key[0..], key_pair[seed_length..]);
return public_key;
}

/// Sign a message using a key pair, and optional random noise.
/// Having noise creates non-standard, non-deterministic signatures,
/// but has been proven to increase resilience against fault attacks.
pub fn sign(msg: []const u8, key_pair: [keypair_length]u8, noise: ?[noise_length]u8) ![signature_length]u8 {
const public_key = key_pair[32..];
var az: [Sha512.digest_length]u8 = undefined;
var h = Sha512.init();
h.update(key_pair[0..seed_length]);
h.final(&az);

h = Sha512.init();
if (noise) |*z| {
h.update(z);
}
h.update(az[32..]);
h.update(msg);
var nonce64: [64]u8 = undefined;
h.final(&nonce64);
const nonce = Curve.scalar.reduce64(nonce64);
const r = try Curve.basePoint.mul(nonce);

var sig: [signature_length]u8 = undefined;
mem.copy(u8, sig[0..32], &r.toBytes());
mem.copy(u8, sig[32..], public_key);
h = Sha512.init();
h.update(&sig);
h.update(msg);
var hram64: [Sha512.digest_length]u8 = undefined;
h.final(&hram64);
const hram = Curve.scalar.reduce64(hram64);

var x = az[0..32];
Curve.scalar.clamp(x);
const s = Curve.scalar.mulAdd(hram, x.*, nonce);
mem.copy(u8, sig[32..], s[0..]);
return sig;
}

/// Verify an Ed25519 signature given a message and a public key.
/// Returns error.InvalidSignature is the signature verification failed.
pub fn verify(sig: [signature_length]u8, msg: []const u8, public_key: [public_length]u8) !void {
const r = sig[0..32];
const s = sig[32..64];
try Curve.scalar.rejectNonCanonical(s.*);
try Curve.rejectNonCanonical(public_key);
const a = try Curve.fromBytes(public_key);
try a.rejectIdentity();

var h = Sha512.init();
h.update(r);
h.update(&public_key);
h.update(msg);
var hram64: [Sha512.digest_length]u8 = undefined;
h.final(&hram64);
const hram = Curve.scalar.reduce64(hram64);

const p = try a.neg().mul(hram);
const check = (try Curve.basePoint.mul(s.*)).add(p).toBytes();
if (mem.eql(u8, &check, r) == false) {
return error.InvalidSignature;
}
}
};

test "ed25519 key pair creation" {
var seed: [32]u8 = undefined;
try fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166");
const key_pair = try Ed25519.createKeyPair(seed);
var buf: [256]u8 = undefined;
std.testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{key_pair}), "8052030376D47112BE7F73ED7A019293DD12AD910B654455798B4667D73DE1662D6F7455D97B4A3A10D7293909D1A4F2058CB9A370E43FA8154BB280DB839083");

const public_key = Ed25519.publicKey(key_pair);
std.testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{public_key}), "2D6F7455D97B4A3A10D7293909D1A4F2058CB9A370E43FA8154BB280DB839083");
}

test "ed25519 signature" {
var seed: [32]u8 = undefined;
try fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166");
const key_pair = try Ed25519.createKeyPair(seed);

const sig = try Ed25519.sign("test", key_pair, null);
var buf: [128]u8 = undefined;
std.testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{sig}), "10A442B4A80CC4225B154F43BEF28D2472CA80221951262EB8E0DF9091575E2687CC486E77263C3418C757522D54F84B0359236ABBBD4ACD20DC297FDCA66808");
const public_key = Ed25519.publicKey(key_pair);
try Ed25519.verify(sig, "test", public_key);
std.testing.expectError(error.InvalidSignature, Ed25519.verify(sig, "TEST", public_key));
}
Loading