Skip to content

Commit 45be49d

Browse files
authored
implement config options (#5)
* implement config options * clean up docs * update dictionary * clean up * address comments; remove varification varints * clean up
1 parent 3407a3f commit 45be49d

11 files changed

Lines changed: 683 additions & 80 deletions

File tree

.config/spellcheck.dic

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
1-
20
1+
33
2+
19AM
23
CPI
4+
Cofactorless
35
Ed25519
46
Ed25519SignatureOffsets
7+
FIPS
58
Mollusk
69
SBF
710
SDK
811
SVM
912
Solana
13+
canonicity
14+
catalogued
15+
cofactor
1016
cofactored
17+
cofactorless
1118
cryptographic
19+
dalek
20+
dalek's
21+
de
1222
deserializes
1323
ed25519
24+
encodings
1425
entrypoint
26+
libsodium
1527
malleability
1628
precompile
1729
precompiles
1830
runtime
31+
selectable
1932
syscall
2033
syscalls
2134
verifier
35+
versa

.config/spellcheck.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22
use_builtin = true
33
search_dirs = ["."]
44
extra_dictionaries = ["spellcheck.dic"]
5-
5+
# Treat the em dash (U+2014) as a token separator; cargo-spellcheck's default
6+
# split chars cover the other dash variants but not this one, so a space-padded
7+
# " — " would otherwise be spell-checked as the word "—".
8+
tokenization_splitchars = "\",';:.!?#(){}[]|/_-‒–⁃⁻₋−⸺⸻\n…—"

README.md

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ succeeded.
2222
| Syscall | SDK wrapper |
2323
|---|---|
2424
| `sol_sha512` | `solana_sha512_hasher::hashv` |
25-
| `sol_curve_group_op` | `solana_curve25519::edwards::multiply_edwards` |
25+
| `sol_curve_group_op` | `solana_curve25519::edwards::{add_edwards, subtract_edwards}` |
2626
| `sol_curve_multiscalar_mul` | `solana_curve25519::edwards::multiscalar_multiply_edwards` |
2727

2828
`sol_sha512` is not live on mainnet yet. The wrapper crate is published as
@@ -31,44 +31,81 @@ feature before SBF execution will work.
3131

3232
## Instruction format
3333

34-
```text
35-
[0] number of signatures (u8)
36-
[1] padding, ignored
37-
[2 .. 2 + 14*N] N x Ed25519SignatureOffsets records (14 bytes each, LE)
38-
[2 + 14*N ..] payload: public keys, signatures, messages (order flexible)
39-
```
40-
41-
Each offset record matches `Ed25519SignatureOffsets` exposed by this crate:
34+
The program verifies a single signature. Instruction data is:
4235

4336
```text
44-
[0..2] signature_offset
45-
[2..4] signature_instruction_index
46-
[4..6] public_key_offset
47-
[6..8] public_key_instruction_index
48-
[8..10] message_data_offset
49-
[10..12] message_data_size
50-
[12..14] message_instruction_index
37+
[0 .. 32] public key A (32 bytes)
38+
[32 .. 96] signature R‖S (64 bytes)
39+
[96 ..] message
5140
```
5241

42+
The `ed25519_verify_instruction` helper in `solana-ed25519-verify` builds this
43+
layout.
44+
5345
### Constraints
5446

55-
- **All instruction-index fields must be `u16::MAX`.** The native precompile
56-
uses this sentinel for the current instruction. An SBF program receives only
57-
its own instruction data; cross-instruction references require a future
58-
runtime change.
59-
- **ZIP-215 verification.** The program uses the cofactored equation
60-
`[8](S·B − H(R‖A‖M)·A) == [8]R` with canonical `S`, following
61-
[ZIP-215](https://zips.z.cash/zip-0215). Small-order `R` and public-key
62-
points are not explicitly rejected — the cofactor multiplication makes them
63-
indistinguishable from the identity contribution and verification fails
64-
naturally for any signature not crafted for them. This is backward compatible
65-
with `ed25519_dalek::verify_strict`: every point accepted by dalek is also
66-
accepted here (dalek rejects small-order points outright, so no valid dalek
67-
signature is broken by the relaxed check).
68-
- **Zero-signature payloads** are accepted only when the buffer is exactly the
69-
2-byte header.
47+
- **Verification criteria.** The program always applies [ZIP-215]: the
48+
cofactored equation `[8](S·B − H(R‖A‖M)·A) == [8]R` with canonical `S`.
49+
Small-order and non-canonical points are accepted. Programs needing a
50+
different variant (e.g. `verify_strict`) should depend on the
51+
`solana-ed25519-verify` library directly (see
52+
[Verification criteria](#verification-criteria-library)).
7053
- **No accounts.** The program takes no account arguments and returns
7154
`InvalidArgument` if any are supplied.
55+
- **Minimum length.** Instruction data shorter than the 96-byte
56+
`A || R‖S` header is rejected with `InvalidInstructionData`.
57+
58+
[ZIP-215]: https://zips.z.cash/zip-0215
59+
60+
## Verification criteria (library)
61+
62+
Ed25519 "validity" is not one definition — implementations differ on cofactoring,
63+
non-canonical encodings, and small-order rejection (see Henry de Valence's
64+
[It's 255:19AM]). The `solana-ed25519-verify` crate exposes these as independent
65+
knobs via `VerificationCriteria`:
66+
67+
| Knob | Effect when enabled | Extra syscalls |
68+
|---|---|---|
69+
| `cofactored` | Use `[8](S·B − H·A − R) == identity` instead of the cofactorless `S·B − H·A − R == identity` | +3 `sol_curve_group_op` (multiply-by-8 as three doublings) |
70+
| `require_canonical_a` | Reject public keys whose `y`-coordinate is `≥ p` | none |
71+
| `require_canonical_r` | Reject signature `R` whose `y`-coordinate is `≥ p` | none |
72+
| `reject_small_order_a` | Reject small-order (torsion) public keys | +3 `sol_curve_group_op` |
73+
| `reject_small_order_r` | Reject small-order signature `R` values | +3 `sol_curve_group_op` |
74+
| `require_canonical_s` | Reject `S ≥ L` | none |
75+
76+
```rust
77+
use solana_ed25519_verify::{Ed25519Verifier, VerificationCriteria};
78+
79+
// Default: the ZIP-215 preset (cofactored, canonical S required).
80+
let verifier = Ed25519Verifier::new();
81+
82+
// `ed25519-dalek`'s verify_strict semantics.
83+
let strict = Ed25519Verifier::with_criteria(VerificationCriteria::dalek_verify_strict());
84+
85+
// Or compose a variant by overriding individual knobs.
86+
let custom = Ed25519Verifier::with_criteria(VerificationCriteria {
87+
reject_small_order_a: true,
88+
..VerificationCriteria::zip215()
89+
});
90+
```
91+
92+
Named presets:
93+
94+
| Preset | `cofactored` | `canonical_a` | `canonical_r` | `small_order_a` | `small_order_r` | `canonical_s` |
95+
|---|---|---|---|---|---|---|
96+
| `zip215()` (default) || | | | ||
97+
| `dalek_verify_strict()` | | |||||
98+
99+
`dalek_verify_strict()` matches `ed25519_dalek::VerifyingKey::verify_strict`
100+
exactly (cross-checked in the test suite), including the detail that a
101+
non-canonically encoded public key `A` is *not* rejected. Further presets
102+
(libsodium, RFC 8032 / FIPS 186-5) can be added in follow-ups.
103+
104+
The on-chain program always applies the `zip215()` preset. A program needing a
105+
different variant should depend on this crate directly and build an
106+
`Ed25519Verifier` from the desired `VerificationCriteria`.
107+
108+
[It's 255:19AM]: https://hdevalence.ca/blog/2020-10-04-its-25519am/
72109

73110
## Build and test
74111

ed25519-verify/src/config.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! Configurable Ed25519 verification criteria.
2+
//!
3+
//! Ed25519 "signature validity" is not a single definition: implementations
4+
//! differ on cofactored vs. cofactorless verification, whether non-canonical
5+
//! point encodings are accepted, and whether small-order points are rejected.
6+
//! These divergences are catalogued in Henry de Valence's
7+
//! ["It's 255:19AM. Do you know what your validation criteria are?"][blog].
8+
//!
9+
//! [`VerificationCriteria`] exposes those divergences as independent knobs so a
10+
//! caller can select the exact variant they need. Two named presets ship today —
11+
//! [`zip215`] (the [ZIP-215] criteria specified by [SIMD-0376]) and
12+
//! [`dalek_verify_strict`] — and the knobs are designed so that other well-known
13+
//! profiles (e.g. libsodium, RFC 8032 / FIPS 186-5) can be added as presets in
14+
//! follow-ups without changing the verifier.
15+
//!
16+
//! [blog]: https://hdevalence.ca/blog/2020-10-04-its-25519am/
17+
//! [ZIP-215]: https://zips.z.cash/zip-0215
18+
//! [SIMD-0376]: https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0376-verify-strict.md
19+
//! [`zip215`]: VerificationCriteria::zip215
20+
//! [`dalek_verify_strict`]: VerificationCriteria::dalek_verify_strict
21+
22+
/// Independent Ed25519 validation knobs.
23+
///
24+
/// Each field toggles one decision point from the "255:19AM" taxonomy. Fields
25+
/// are public so callers can compose arbitrary combinations, typically by
26+
/// starting from a preset and overriding a single knob:
27+
///
28+
/// ```
29+
/// use solana_ed25519_verify::VerificationCriteria;
30+
///
31+
/// let strict_s = VerificationCriteria {
32+
/// reject_small_order_a: true,
33+
/// ..VerificationCriteria::zip215()
34+
/// };
35+
/// ```
36+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37+
pub struct VerificationCriteria {
38+
/// Use the cofactored verification equation `[8](S·B − H·A) == [8]R`.
39+
///
40+
/// When `false`, the cofactorless equation `S·B − H·A == R` is used, which
41+
/// rejects mixed-order points that the cofactored equation tolerates. The
42+
/// cofactored form costs one extra multiplication by the cofactor 8, which
43+
/// the verifier performs as three `sol_curve_group_op` additions.
44+
pub cofactored: bool,
45+
/// Reject public keys whose compressed `y`-coordinate is `>= p` (a
46+
/// non-canonical encoding of a reduced point).
47+
pub require_canonical_a: bool,
48+
/// Reject signature `R` values whose compressed `y`-coordinate is `>= p`.
49+
pub require_canonical_r: bool,
50+
/// Reject public keys that lie in the small-order (torsion) subgroup.
51+
///
52+
/// Costs a multiplication by the cofactor 8 (three `sol_curve_group_op`
53+
/// additions) when enabled.
54+
pub reject_small_order_a: bool,
55+
/// Reject signature `R` values that lie in the small-order subgroup.
56+
///
57+
/// Costs a multiplication by the cofactor 8 (three `sol_curve_group_op`
58+
/// additions) when enabled.
59+
pub reject_small_order_r: bool,
60+
/// Reject signatures whose scalar `S` is not in canonical `[0, L)` form.
61+
pub require_canonical_s: bool,
62+
}
63+
64+
impl VerificationCriteria {
65+
/// [ZIP-215] verification, as specified for Solana by [SIMD-0376].
66+
///
67+
/// Cofactored equation with a canonical `S` requirement; non-canonical point
68+
/// encodings and small-order points are accepted (cofactor multiplication
69+
/// makes them indistinguishable from the identity contribution). This is
70+
/// backward compatible with `ed25519_dalek::verify_strict`: every signature
71+
/// dalek accepts is accepted here.
72+
///
73+
/// [ZIP-215]: https://zips.z.cash/zip-0215
74+
/// [SIMD-0376]: https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0376-verify-strict.md
75+
pub const fn zip215() -> Self {
76+
Self {
77+
cofactored: true,
78+
require_canonical_a: false,
79+
require_canonical_r: false,
80+
reject_small_order_a: false,
81+
reject_small_order_r: false,
82+
require_canonical_s: true,
83+
}
84+
}
85+
86+
/// The criteria enforced by `ed25519_dalek::VerifyingKey::verify_strict`.
87+
///
88+
/// Cofactorless verification with canonical `S`, canonical `R`, and
89+
/// small-order rejection for both `A` and `R`. Mirrors ed25519-dalek 2.x
90+
/// exactly, including the detail that a non-canonically encoded public key
91+
/// `A` is *not* rejected — dalek's `VerifyingKey::from_bytes` decompresses
92+
/// `A` (reducing `y` modulo `p`) without a canonicity check, and
93+
/// `verify_strict` only re-encodes and compares `R`. Every signature this
94+
/// preset accepts is accepted by dalek's `verify_strict`, and vice versa.
95+
pub const fn dalek_verify_strict() -> Self {
96+
Self {
97+
cofactored: false,
98+
require_canonical_a: false,
99+
require_canonical_r: true,
100+
reject_small_order_a: true,
101+
reject_small_order_r: true,
102+
require_canonical_s: true,
103+
}
104+
}
105+
}
106+
107+
impl Default for VerificationCriteria {
108+
fn default() -> Self {
109+
Self::zip215()
110+
}
111+
}

ed25519-verify/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@
66
//! `solana-ed25519-program`. Programs can also depend on it directly to verify
77
//! Ed25519 signatures without invoking the standalone verifier program.
88
//!
9-
//! The verifier performs ZIP-215 verification with canonical `S`.
9+
//! By default the verifier performs ZIP-215 verification with canonical `S`.
10+
//! The variant can be selected via [`VerificationCriteria`] and
11+
//! [`Ed25519Verifier::with_criteria`].
1012
1113
#[cfg(feature = "instruction")]
1214
extern crate alloc;
1315

16+
mod config;
1417
#[cfg(feature = "instruction")]
1518
pub mod program;
1619
mod scalar;
1720
mod verifier;
1821

22+
pub use config::VerificationCriteria;
1923
#[cfg(feature = "instruction")]
2024
pub use program::ed25519_verify_instruction;
2125
pub use verifier::Ed25519Verifier;

ed25519-verify/src/program.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ use {
88
};
99

1010
/// Constructs an on-chain instruction to invoke `solana-ed25519-program`.
11+
///
12+
/// The instruction data is `public_key || signature || message`. The program
13+
/// verifies the signature under the [ZIP-215] criteria.
14+
///
15+
/// [ZIP-215]: crate::VerificationCriteria::zip215
1116
pub fn ed25519_verify_instruction(
1217
program_id: &Pubkey,
1318
public_key: &[u8; PUBKEY_SERIALIZED_SIZE],

ed25519-verify/src/scalar.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
//! Small scalar helpers needed to assemble Ed25519 verification around syscalls.
1+
//! Small scalar- and field-element helpers needed to assemble Ed25519
2+
//! verification around syscalls.
23
34
/// Group order of the ed25519 base point in little-endian form:
45
/// `2^252 + 27742317777372353535851937790883648493`.
@@ -7,11 +8,30 @@ pub(crate) const BASEPOINT_ORDER: [u8; 32] = [
78
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
89
];
910

11+
/// Field modulus `p = 2^255 - 19` in little-endian form.
12+
const FIELD_MODULUS: [u8; 32] = [
13+
0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
14+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
15+
];
16+
1017
/// Returns `true` if `scalar` is in canonical `[0, L)` form.
1118
pub(crate) fn is_canonical_scalar(scalar: &[u8; 32]) -> bool {
1219
cmp_le(scalar, &BASEPOINT_ORDER).is_lt()
1320
}
1421

22+
/// Returns `true` if `encoding` is a canonical compressed Edwards point.
23+
///
24+
/// A compressed point stores the `y`-coordinate in the low 255 bits and the
25+
/// sign of `x` in the top bit. An encoding is canonical when the masked
26+
/// `y`-coordinate is a reduced field element (`y < p`). Non-canonical encodings
27+
/// (`y >= p`) still decompress — they reduce modulo `p` first — but represent a
28+
/// point with an alternative, non-reduced serialization.
29+
pub(crate) fn is_canonical_point_encoding(encoding: &[u8; 32]) -> bool {
30+
let mut y = *encoding;
31+
y[31] &= 0x7f;
32+
cmp_le(&y, &FIELD_MODULUS).is_lt()
33+
}
34+
1535
/// Reduces a 64-byte little-endian integer modulo the ed25519 base point order.
1636
pub(crate) fn reduce_wide(wide: &[u8; 64]) -> [u8; 32] {
1737
let mut remainder = [0u8; 32];
@@ -64,7 +84,7 @@ fn sub_assign(left: &mut [u8; 32], right: &[u8; 32]) {
6484
}
6585
}
6686

67-
fn cmp_le(left: &[u8; 32], right: &[u8; 32]) -> core::cmp::Ordering {
87+
pub(crate) fn cmp_le(left: &[u8; 32], right: &[u8; 32]) -> core::cmp::Ordering {
6888
for (left_byte, right_byte) in left.iter().zip(right).rev() {
6989
match left_byte.cmp(right_byte) {
7090
core::cmp::Ordering::Equal => {}
@@ -98,4 +118,33 @@ mod tests {
98118
fn negates_zero_to_zero() {
99119
assert_eq!(negate(&[0; 32]), [0; 32]);
100120
}
121+
122+
#[test]
123+
fn accepts_reduced_encodings() {
124+
// y = 0
125+
assert!(is_canonical_point_encoding(&[0; 32]));
126+
127+
// y = p - 1 (the small-order point (0, -1)), with and without sign bit.
128+
let mut y = FIELD_MODULUS;
129+
y[0] -= 1;
130+
assert!(is_canonical_point_encoding(&y));
131+
y[31] |= 0x80;
132+
assert!(is_canonical_point_encoding(&y));
133+
}
134+
135+
#[test]
136+
fn rejects_unreduced_encodings() {
137+
// y = p
138+
assert!(!is_canonical_point_encoding(&FIELD_MODULUS));
139+
140+
// y = p, sign bit set (the sign bit must be ignored, so still rejected).
141+
let mut y = FIELD_MODULUS;
142+
y[31] |= 0x80;
143+
assert!(!is_canonical_point_encoding(&y));
144+
145+
// y = 2^255 - 1 (largest value the 255 bits can hold, > p).
146+
let mut y = [0xff; 32];
147+
y[31] = 0x7f;
148+
assert!(!is_canonical_point_encoding(&y));
149+
}
101150
}

0 commit comments

Comments
 (0)