Skip to content

Commit 8e1e24a

Browse files
Michael LodderJared Stanbrough
authored andcommitted
feat(rust): add bbs+ signature scheme
Signed-off-by: Michael Lodder <mike@ockam.io>
1 parent 72e3817 commit 8e1e24a

26 files changed

Lines changed: 1799 additions & 5 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "bbs"
3+
version = "0.1.0"
4+
authors = ["Ockam Developers"]
5+
edition = "2018"
6+
license = "Apache-2.0"
7+
homepage = "https://github.com/ockam-network/ockam"
8+
repository = "https://github.com/ockam-network/ockam/tree/develop/implementations/rust/ockam/ockam_signature_bbs"
9+
readme = "README.md"
10+
categories = ["cryptography", "asynchronous", "authentication","no-std","algorithms"]
11+
keywords = ["ockam", "crypto", "signature", "signing", "bls"]
12+
description = """The Ockam BLS signature impementation.
13+
"""
14+
15+
[features]
16+
17+
[dependencies]
18+
bls = { version = "0.1", path = "../ockam_signature_bls", package = "ockam_signature_bls" }
19+
bls12_381_plus = "0.4"
20+
blake2 = { version = "0.9", default-features = false }
21+
digest = { version = "0.9", default-features = false }
22+
ff = "0.9"
23+
group = "0.9"
24+
hmac-drbg = "0.3"
25+
managed = { version = "0.8", features = ["map"] }
26+
pairing = "0.19"
27+
rand_core = "0.6"
28+
serde = { version = "1.0", features = ["derive"] }
29+
short_group_signatures_core = { version = "0.1", path = "../ockam_signature_core", package = "ockam_signature_core" }
30+
subtle = { version = "2.4", default-features = false }
31+
typenum = "1.13"
32+
zeroize = { version = "1.2", features = ["zeroize_derive"] }
33+
34+
[dev-dependencies]
35+
rand_xorshift = "0.3"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../DEVELOP.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../LICENSE
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ockam\_signature\_bbs
2+
3+
[![crate][crate-image]][crate-link]
4+
[![docs][docs-image]][docs-link]
5+
[![license][license-image]][license-link]
6+
[![discuss][discuss-image]][discuss-link]
7+
8+
Ockam is a library for building devices that communicate securely, privately
9+
and trustfully with cloud services and other devices.
10+
11+
In order to support a variety of proving protocols, this crate implements the BBS+ signature scheme which can be used to generate zero-knowledge proofs about signed attributes and the signatures themselves.
12+
13+
The main [Ockam][main-ockam-crate-link] has optional dependency on this crate.
14+
15+
## Usage
16+
17+
Add this to your `Cargo.toml`:
18+
19+
```
20+
[dependencies]
21+
ockam_signature_bbs = "0.1.0"
22+
```
23+
24+
## Crate Features
25+
26+
```
27+
[dependencies]
28+
ockam_signature_bbs = { version = "0.1.0", default-features = false }
29+
```
30+
31+
Please note that Cargo features are unioned across the entire dependency
32+
graph of a project. If any other crate you depend on has not opted out of
33+
`ockam_signature_bbs` default features, Cargo will build `ockam_signature_bbs` with the std
34+
feature enabled whether or not your direct dependency on `ockam_signature_bbs`
35+
has `default-features = false`.
36+
37+
## License
38+
39+
This code is licensed under the terms of the [Apache License 2.0][license-link].
40+
41+
[main-ockam-crate-link]: https://crates.io/crates/ockam
42+
[ockam-vault-crate-link]: https://crates.io/crates/ockam_signature_bbs
43+
44+
[crate-image]: https://img.shields.io/crates/v/ockam_signature_bbs.svg
45+
[crate-link]: https://crates.io/crates/ockam_signature_bbs
46+
47+
[docs-image]: https://docs.rs/ockam_signature_bbs/badge.svg
48+
[docs-link]: https://docs.rs/ockam_signature_bbs
49+
50+
[license-image]: https://img.shields.io/badge/License-Apache%202.0-green.svg
51+
[license-link]: https://github.com/ockam-network/ockam/blob/HEAD/LICENSE
52+
53+
[discuss-image]: https://img.shields.io/badge/Discuss-Github%20Discussions-ff70b4.svg
54+
[discuss-link]: https://github.com/ockam-network/ockam/discussions
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use crate::{Commitment, Message, MessageGenerators, Signature, SignatureBlinding, MAX_MSGS};
2+
use blake2::Blake2b;
3+
use bls::SecretKey;
4+
use bls12_381_plus::{G1Projective, Scalar};
5+
use core::convert::TryFrom;
6+
use digest::Digest;
7+
use ff::Field;
8+
use group::Curve;
9+
use hmac_drbg::HmacDRBG;
10+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
11+
use short_group_signatures_core::{error::Error, lib::*};
12+
use subtle::CtOption;
13+
use typenum::{marker_traits::NonZero, U64};
14+
15+
/// A BBS+ blind signature
16+
/// structurally identical to `Signature` but is used to
17+
/// help with misuse and confusion.
18+
///
19+
/// 1 or more messages have been hidden by the signature recipient
20+
/// so the signer only knows a subset of the messages to be signed
21+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
22+
pub struct BlindSignature {
23+
pub(crate) a: G1Projective,
24+
pub(crate) e: Scalar,
25+
pub(crate) s: Scalar,
26+
}
27+
28+
impl Serialize for BlindSignature {
29+
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
30+
where
31+
S: Serializer,
32+
{
33+
let sig = Signature {
34+
a: self.a,
35+
e: self.e,
36+
s: self.s,
37+
};
38+
sig.serialize(s)
39+
}
40+
}
41+
42+
impl<'de> Deserialize<'de> for BlindSignature {
43+
fn deserialize<D>(d: D) -> Result<BlindSignature, D::Error>
44+
where
45+
D: Deserializer<'de>,
46+
{
47+
let sig = Signature::deserialize(d)?;
48+
Ok(Self {
49+
a: sig.a,
50+
e: sig.e,
51+
s: sig.s,
52+
})
53+
}
54+
}
55+
56+
impl BlindSignature {
57+
/// The number of bytes in a signature
58+
pub const BYTES: usize = 112;
59+
60+
/// Generate a blind signature where only a subset of messages are known to the signer
61+
/// The rest are encoded as a commitment
62+
pub fn new<N>(
63+
commitment: Commitment,
64+
sk: &SecretKey,
65+
generators: &MessageGenerators<N>,
66+
msgs: &[(usize, Message)],
67+
) -> Result<Self, Error>
68+
where
69+
N: ArrayLength<G1Projective> + NonZero,
70+
{
71+
if N::to_usize() < msgs.len() {
72+
return Err(Error::new(1, "not enough message generators"));
73+
}
74+
if sk.0.is_zero() {
75+
return Err(Error::new(2, "invalid secret key"));
76+
}
77+
78+
let mut hasher = Blake2b::new();
79+
hasher.update(generators.h0.to_affine().to_uncompressed());
80+
for h in &generators.h {
81+
hasher.update(h.to_affine().to_uncompressed());
82+
}
83+
for (_, m) in msgs.iter() {
84+
hasher.update(m.to_bytes())
85+
}
86+
let nonce = hasher.finalize();
87+
let mut drbg = HmacDRBG::<Blake2b>::new(&sk.to_bytes()[..], &nonce[..], &[]);
88+
// Should yield non-zero values for `e` and `s`, very small likelihood of it being zero
89+
let e = Scalar::from_bytes_wide(
90+
&<[u8; 64]>::try_from(&drbg.generate::<U64>(Some(&[1u8]))[..]).unwrap(),
91+
);
92+
let s = Scalar::from_bytes_wide(
93+
&<[u8; 64]>::try_from(&drbg.generate::<U64>(Some(&[2u8]))[..]).unwrap(),
94+
);
95+
96+
// Can't go more than 128, but that's quite a bit
97+
let mut points = [G1Projective::identity(); MAX_MSGS];
98+
let mut scalars = [Scalar::one(); MAX_MSGS];
99+
100+
points[0] = commitment.0;
101+
points[1] = G1Projective::generator();
102+
points[2] = generators.h0;
103+
scalars[2] = s;
104+
105+
let mut i = 3;
106+
for (idx, m) in msgs.iter() {
107+
points[i] = generators.h[*idx];
108+
scalars[i] = m.0;
109+
i += 1;
110+
}
111+
112+
let b = G1Projective::sum_of_products(&points[..i], &scalars[..i]);
113+
let exp = (e + sk.0).invert().unwrap();
114+
115+
Ok(Self { a: b * exp, e, s })
116+
}
117+
118+
/// Once signature on committed attributes (blind signature) is received, the signature needs to be unblinded.
119+
/// Takes the blinding factor used in the commitment.
120+
pub fn to_unblinded(self, blinding: SignatureBlinding) -> Signature {
121+
Signature {
122+
a: self.a,
123+
e: self.e,
124+
s: self.s + blinding.0,
125+
}
126+
}
127+
128+
/// Get the byte representation of this signature
129+
pub fn to_bytes(&self) -> [u8; Self::BYTES] {
130+
let sig = Signature {
131+
a: self.a,
132+
e: self.e,
133+
s: self.s,
134+
};
135+
sig.to_bytes()
136+
}
137+
138+
/// Convert a byte sequence into a signature
139+
pub fn from_bytes(data: &[u8; Self::BYTES]) -> CtOption<Self> {
140+
Signature::from_bytes(data).map(|sig| Self {
141+
a: sig.a,
142+
e: sig.e,
143+
s: sig.s,
144+
})
145+
}
146+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use crate::{Challenge, Commitment, MessageGenerators, Nonce, COMMITMENT_BYTES, FIELD_BYTES};
2+
use blake2::VarBlake2b;
3+
use bls12_381_plus::{G1Projective, Scalar};
4+
use core::convert::TryFrom;
5+
use digest::{Update, VariableOutput};
6+
use group::Curve;
7+
use short_group_signatures_core::{error::Error, lib::*};
8+
use subtle::ConstantTimeEq;
9+
use typenum::NonZero;
10+
11+
/// Contains the data used for computing a blind signature and verifying
12+
/// proof of hidden messages from a prover
13+
#[derive(Debug, Clone)]
14+
pub struct BlindSignatureContext {
15+
/// The blinded signature commitment
16+
pub commitment: Commitment,
17+
/// The challenge hash for the Fiat-Shamir heuristic
18+
pub challenge: Challenge,
19+
/// The proofs for the hidden messages
20+
pub proofs: Vec<Challenge, U16>,
21+
}
22+
23+
impl BlindSignatureContext {
24+
/// Store the generators as a sequence of bytes
25+
/// Each point is compressed to big-endian format
26+
/// Needs (N + 1) * 32 + 48 * 2 space otherwise it will panic
27+
pub fn to_bytes(&self, buffer: &mut [u8]) {
28+
buffer[0..COMMITMENT_BYTES].copy_from_slice(&self.commitment.to_bytes());
29+
let mut offset = COMMITMENT_BYTES;
30+
let mut end = offset + FIELD_BYTES;
31+
32+
buffer[offset..end].copy_from_slice(&self.challenge.to_bytes());
33+
34+
offset = end;
35+
end += FIELD_BYTES;
36+
37+
for i in 0..self.proofs.len() {
38+
buffer[offset..end].copy_from_slice(&self.proofs[i].to_bytes());
39+
offset = end;
40+
end += FIELD_BYTES;
41+
}
42+
}
43+
44+
/// Convert a byte sequence into the blind signature context
45+
/// Expected size is (N + 1) * 32 + 48 bytes
46+
pub fn from_bytes<B: AsRef<[u8]>>(bytes: B) -> Option<Self> {
47+
let size = FIELD_BYTES * 2 + COMMITMENT_BYTES;
48+
let buffer = bytes.as_ref();
49+
if buffer.len() < size {
50+
return None;
51+
}
52+
if buffer.len() - COMMITMENT_BYTES % FIELD_BYTES != 0 {
53+
return None;
54+
}
55+
56+
let commitment =
57+
Commitment::from_bytes(slicer!(buffer, 0, COMMITMENT_BYTES, COMMITMENT_BYTES));
58+
if commitment.is_none().unwrap_u8() == 1 {
59+
return None;
60+
}
61+
let mut offset = COMMITMENT_BYTES;
62+
let mut end = COMMITMENT_BYTES + FIELD_BYTES;
63+
64+
let challenge = Challenge::from_bytes(slicer!(buffer, offset, end, FIELD_BYTES));
65+
if challenge.is_none().unwrap_u8() == 1 {
66+
return None;
67+
}
68+
69+
let times = (buffer.len() - COMMITMENT_BYTES - FIELD_BYTES) / FIELD_BYTES;
70+
71+
offset = end;
72+
end += FIELD_BYTES;
73+
74+
let mut proofs = Vec::<Challenge, U16>::new();
75+
for _ in 0..times {
76+
let p = Challenge::from_bytes(slicer!(buffer, offset, end, FIELD_BYTES));
77+
if p.is_none().unwrap_u8() == 1 {
78+
return None;
79+
}
80+
proofs.push(p.unwrap()).unwrap();
81+
offset = end;
82+
end += FIELD_BYTES;
83+
}
84+
85+
Some(Self {
86+
commitment: commitment.unwrap(),
87+
challenge: challenge.unwrap(),
88+
proofs,
89+
})
90+
}
91+
92+
/// Assumes the proof of hidden messages
93+
/// If other proofs were included, those will need to be verified another way
94+
pub fn verify<S>(
95+
&self,
96+
known_messages: &[usize],
97+
generators: &MessageGenerators<S>,
98+
nonce: Nonce,
99+
) -> Result<bool, Error>
100+
where
101+
S: ArrayLength<G1Projective> + NonZero,
102+
{
103+
let mut known = HashSet::new();
104+
let mut points = Vec::<G1Projective, U32>::new();
105+
for idx in known_messages {
106+
if *idx >= S::to_usize() {
107+
return Err(Error::new(1, "index out of bounds"));
108+
}
109+
known.insert(*idx);
110+
}
111+
for i in 0..S::to_usize() {
112+
if !known.contains(&i) {
113+
points
114+
.push(generators.h[i])
115+
.map_err(|_| Error::new((i + 1) as u32, "allocate more space"))?;
116+
}
117+
}
118+
points
119+
.push(generators.h0)
120+
.map_err(|_| Error::new(S::to_u32(), "allocate more space"))?;
121+
points
122+
.push(self.commitment.0)
123+
.map_err(|_| Error::new(S::to_u32(), "allocate more space"))?;
124+
125+
let mut scalars = self
126+
.proofs
127+
.iter()
128+
.map(|p| p.0)
129+
.collect::<Vec<Scalar, U32>>();
130+
scalars
131+
.push(self.challenge.0.neg())
132+
.map_err(|_| Error::new(S::to_u32(), "allocate more space"))?;
133+
134+
let mut res = [0u8; COMMITMENT_BYTES];
135+
let mut hasher = VarBlake2b::new(COMMITMENT_BYTES).unwrap();
136+
137+
let commitment = crate::util::sum_of_products(points.as_ref(), scalars.as_mut());
138+
hasher.update(&commitment.to_affine().to_uncompressed());
139+
hasher.update(&self.commitment.0.to_affine().to_uncompressed());
140+
hasher.update(nonce.to_bytes());
141+
hasher.finalize_variable(|out| {
142+
res.copy_from_slice(out);
143+
});
144+
let challenge = Scalar::from_okm(&res);
145+
146+
Ok(self.challenge.0.ct_eq(&challenge).unwrap_u8() == 1)
147+
}
148+
}

0 commit comments

Comments
 (0)