Skip to content

Commit 3c28ad3

Browse files
Merge pull request #55 from bdewater/eddsa
Add EdDSA support
2 parents abc42a1 + 42d3f04 commit 3c28ad3

9 files changed

Lines changed: 191 additions & 0 deletions

File tree

lib/cose/algorithm.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "cose/algorithm/ecdsa"
4+
require "cose/algorithm/eddsa"
45
require "cose/algorithm/hmac"
56
require "cose/algorithm/rsa_pss"
67

@@ -30,6 +31,7 @@ def self.by_name(name)
3031
register(ECDSA.new(-35, "ES384", hash_function: "SHA384", curve_name: "P-384"))
3132
register(ECDSA.new(-36, "ES512", hash_function: "SHA512", curve_name: "P-521"))
3233
register(ECDSA.new(-47, "ES256K", hash_function: "SHA256", curve_name: "secp256k1"))
34+
register(EdDSA.new(-8, "EdDSA"))
3335
register(RSAPSS.new(-37, "PS256", hash_function: "SHA256", salt_length: 32))
3436
register(RSAPSS.new(-38, "PS384", hash_function: "SHA384", salt_length: 48))
3537
register(RSAPSS.new(-39, "PS512", hash_function: "SHA512", salt_length: 64))

lib/cose/algorithm/eddsa.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
require "cose/algorithm/signature_algorithm"
4+
require "cose/error"
5+
require "cose/key/okp"
6+
require "openssl"
7+
8+
module COSE
9+
module Algorithm
10+
class EdDSA < SignatureAlgorithm
11+
private
12+
13+
def valid_key?(key)
14+
cose_key = to_cose_key(key)
15+
16+
cose_key.is_a?(COSE::Key::OKP) && (!cose_key.alg || cose_key.alg == id)
17+
end
18+
19+
def to_pkey(key)
20+
case key
21+
when COSE::Key::OKP
22+
key.to_pkey
23+
when OpenSSL::PKey::PKey
24+
key
25+
else
26+
raise(COSE::Error, "Incompatible key for algorithm")
27+
end
28+
end
29+
30+
def valid_signature?(key, signature, verification_data)
31+
pkey = to_pkey(key)
32+
33+
begin
34+
pkey.verify(nil, signature, verification_data)
35+
rescue OpenSSL::PKey::PKeyError
36+
false
37+
end
38+
end
39+
end
40+
end
41+
end

lib/cose/key.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def self.from_pkey(pkey)
2424
COSE::Key::EC2.from_pkey(pkey)
2525
when OpenSSL::PKey::RSA
2626
COSE::Key::RSA.from_pkey(pkey)
27+
when OpenSSL::PKey::PKey
28+
COSE::Key::OKP.from_pkey(pkey)
2729
else
2830
raise "Unsupported #{pkey.class} object"
2931
end

lib/cose/key/curve.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ def value
3232
COSE::Key::Curve.register(1, "P-256", "prime256v1")
3333
COSE::Key::Curve.register(2, "P-384", "secp384r1")
3434
COSE::Key::Curve.register(3, "P-521", "secp521r1")
35+
COSE::Key::Curve.register(6, "Ed25519", "ED25519")
36+
COSE::Key::Curve.register(7, "Ed448", "ED448")
3537
COSE::Key::Curve.register(8, "secp256k1", "secp256k1")

lib/cose/key/okp.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "cose/key/curve"
34
require "cose/key/curve_key"
45
require "openssl"
56

@@ -14,9 +15,56 @@ def self.enforce_type(map)
1415
end
1516
end
1617

18+
def self.from_pkey(pkey)
19+
curve = Curve.by_pkey_name(pkey.oid) || raise("Unsupported edwards curve #{pkey.oid}")
20+
attributes = { crv: curve.id }
21+
22+
asymmetric_key = pkey.public_to_der
23+
public_key_bit_string = OpenSSL::ASN1.decode(asymmetric_key).value.last.value
24+
attributes[:x] = public_key_bit_string
25+
begin
26+
asymmetric_key = pkey.private_to_der
27+
private_key = OpenSSL::ASN1.decode(asymmetric_key).value.last.value
28+
curve_private_key = OpenSSL::ASN1.decode(private_key).value
29+
attributes[:d] = curve_private_key
30+
rescue OpenSSL::PKey::PKeyError
31+
# work around lack of https://github.com/ruby/openssl/pull/527, otherwise raises this error
32+
# with message 'i2d_PKCS8PrivateKey_bio: error converting private key' for public keys
33+
nil
34+
end
35+
36+
new(**attributes)
37+
end
38+
1739
def map
1840
super.merge(LABEL_KTY => KTY_OKP)
1941
end
42+
43+
def to_pkey
44+
if curve
45+
private_key_algo = OpenSSL::ASN1::Sequence.new(
46+
[OpenSSL::ASN1::ObjectId.new(curve.pkey_name)]
47+
)
48+
seq = if d
49+
version = OpenSSL::ASN1::Integer.new(0)
50+
curve_private_key = OpenSSL::ASN1::OctetString.new(d).to_der
51+
private_key = OpenSSL::ASN1::OctetString.new(curve_private_key)
52+
[version, private_key_algo, private_key]
53+
else
54+
public_key = OpenSSL::ASN1::BitString.new(x)
55+
[private_key_algo, public_key]
56+
end
57+
58+
asymmetric_key = OpenSSL::ASN1::Sequence.new(seq)
59+
OpenSSL::PKey.read(asymmetric_key.to_der)
60+
else
61+
raise "Unsupported curve #{crv}"
62+
end
63+
end
64+
65+
def curve
66+
Curve.find(crv)
67+
end
2068
end
2169
end
2270
end

spec/cose/key/okp_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,50 @@
3838
end
3939
end
4040

41+
context "#to_pkey" do
42+
if curve_25519_supported?
43+
it "works for an Ed25519 private key" do
44+
original_pkey = OpenSSL::PKey.generate_key("ED25519")
45+
pkey = COSE::Key::OKP.from_pkey(original_pkey).to_pkey
46+
47+
expect(pkey).to be_a(OpenSSL::PKey::PKey)
48+
expect(pkey.oid).to eq("ED25519")
49+
expect(pkey.public_to_der).to eq(original_pkey.public_to_der)
50+
expect(pkey.private_to_der).to eq(original_pkey.private_to_der)
51+
end
52+
53+
it "works for an Ed25519 public key" do
54+
original_pkey = OpenSSL::PKey.generate_key("ED25519")
55+
public_key = OpenSSL::PKey.read(original_pkey.public_to_der)
56+
pkey = COSE::Key::OKP.from_pkey(public_key).to_pkey
57+
58+
expect(pkey).to be_a(OpenSSL::PKey::PKey)
59+
expect(pkey.oid).to eq("ED25519")
60+
expect(pkey.public_to_der).to eq(original_pkey.public_to_der)
61+
end
62+
63+
it "works for an Ed448 private key" do
64+
original_pkey = OpenSSL::PKey.generate_key("ED448")
65+
pkey = COSE::Key::OKP.from_pkey(original_pkey).to_pkey
66+
67+
expect(pkey).to be_a(OpenSSL::PKey::PKey)
68+
expect(pkey.oid).to eq("ED448")
69+
expect(pkey.public_to_der).to eq(original_pkey.public_to_der)
70+
expect(pkey.private_to_der).to eq(original_pkey.private_to_der)
71+
end
72+
73+
it "works for an Ed448 public key" do
74+
original_pkey = OpenSSL::PKey.generate_key("ED448")
75+
public_key = OpenSSL::PKey.read(original_pkey.public_to_der)
76+
pkey = COSE::Key::OKP.from_pkey(public_key).to_pkey
77+
78+
expect(pkey).to be_a(OpenSSL::PKey::PKey)
79+
expect(pkey.oid).to eq("ED448")
80+
expect(pkey.public_to_der).to eq(original_pkey.public_to_der)
81+
end
82+
end
83+
end
84+
4185
describe ".deserialize" do
4286
it "works" do
4387
key = COSE::Key::OKP.deserialize(

spec/cose/sign1_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,29 @@
7878
end
7979
end
8080
end
81+
82+
if curve_25519_supported?
83+
wg_examples("eddsa-examples/eddsa-sig-*.json") do |example|
84+
it "passes #{example['title']}" do
85+
key_data = example["input"]["sign0"]["key"]
86+
87+
key = COSE::Key::OKP.new(
88+
kid: key_data["kid"],
89+
alg: COSE::Algorithm.by_name(example["input"]["sign0"]["alg"]).id,
90+
crv: COSE::Key::Curve.by_name(key_data["crv"]).id,
91+
x: hex_to_bytes(key_data["x_hex"]),
92+
d: hex_to_bytes(key_data["d_hex"])
93+
)
94+
95+
cbor = hex_to_bytes(example["output"]["cbor"])
96+
97+
if example["fail"]
98+
expect { COSE::Sign1.deserialize(cbor).verify(key) }.to raise_error(COSE::Error)
99+
else
100+
expect(COSE::Sign1.deserialize(cbor).verify(key)).to be_truthy
101+
end
102+
end
103+
end
104+
end
81105
end
82106
end

spec/cose/sign_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,29 @@
8787
end
8888
end
8989

90+
if curve_25519_supported?
91+
wg_examples("eddsa-examples/eddsa-0*.json") do |example|
92+
it "passes #{example['title']}" do
93+
key_data = example["input"]["sign"]["signers"][0]["key"]
94+
95+
key = COSE::Key::OKP.new(
96+
kid: key_data["kid"],
97+
crv: COSE::Key::Curve.by_name(key_data["crv"]).id,
98+
x: hex_to_bytes(key_data["x_hex"]),
99+
d: hex_to_bytes(key_data["d_hex"])
100+
)
101+
102+
cbor = hex_to_bytes(example["output"]["cbor"])
103+
104+
if example["fail"]
105+
expect { COSE::Sign.deserialize(cbor).verify(key) }.to raise_error(COSE::Error)
106+
else
107+
expect(COSE::Sign.deserialize(cbor).verify(key)).to be_truthy
108+
end
109+
end
110+
end
111+
end
112+
90113
if rsa_pss_supported?
91114
wg_examples("rsa-pss-examples/*.json") do |example|
92115
it "passes #{example['title']}" do

spec/spec_helper.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ def rsa_pss_supported?
3030
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss)
3131
end
3232

33+
def curve_25519_supported?
34+
OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10101000 && # >= v1.1.1
35+
defined?(OpenSSL::PKey.generate_key)
36+
end
37+
3338
def hex_to_bytes(hex_string)
3439
[hex_string].pack("H*")
3540
end

0 commit comments

Comments
 (0)