1"""
2<Module Name>
3 eddsa.py
4
5<Author>
6 Lukas Puehringer <lukas.puehringer@nyu.edu>
7
8<Started>
9 Oct 22, 2019
10
11<Copyright>
12 See LICENSE for licensing information.
13
14<Purpose>
15 EdDSA/ed25519 algorithm-specific handling routines for pubkey and signature
16 parsing and verification.
17
18"""
19
20import binascii
21
22from securesystemslib import exceptions
23from securesystemslib._gpg import util as gpg_util
24from securesystemslib._gpg.exceptions import PacketParsingError
25
26CRYPTO = True
27NO_CRYPTO_MSG = "EdDSA key support for GPG requires the cryptography library"
28try:
29 from cryptography.exceptions import InvalidSignature
30 from cryptography.hazmat.primitives.asymmetric import (
31 ed25519 as pyca_ed25519,
32 )
33except ImportError:
34 CRYPTO = False
35
36# ECC Curve OID (see RFC4880-bis8 9.2.)
37ED25519_PUBLIC_KEY_OID = bytearray.fromhex("2B 06 01 04 01 DA 47 0F 01")
38
39# EdDSA Point Format (see RFC4880-bis8 13.3.)
40ED25519_PUBLIC_KEY_LENGTH = 33
41ED25519_PUBLIC_KEY_PREFIX = 0x40
42# EdDSA signature byte length (see RFC 8032 5.1.6. (6))
43ED25519_SIG_LENGTH = 64
44
45
46def get_pubkey_params(data):
47 """
48 <Purpose>
49 Parse algorithm-specific part for EdDSA public keys
50
51 See RFC4880-bis8 sections 5.6.5. Algorithm-Specific Part for EdDSA Keys,
52 9.2. ECC Curve OID and 13.3. EdDSA Point Format for more details.
53
54 <Arguments>
55 data:
56 The EdDSA public key data AFTER the one-octet number denoting the
57 public-key algorithm of this key.
58
59 <Exceptions>
60 securesystemslib._gpg.exceptions.PacketParsingError or IndexError:
61 if the public key data is malformed.
62
63 <Side Effects>
64 None.
65
66 <Returns>
67 A dictionary with an element "q" that holds the ascii hex representation
68 of the MPI of an EC point representing an EdDSA public key.
69
70 """
71 ptr = 0
72
73 curve_oid_len = data[ptr]
74 ptr += 1
75
76 curve_oid = data[ptr : ptr + curve_oid_len]
77 ptr += curve_oid_len
78
79 # See 9.2. ECC Curve OID
80 if curve_oid != ED25519_PUBLIC_KEY_OID:
81 raise PacketParsingError(
82 f"bad ed25519 curve OID '{curve_oid}', expected {ED25519_PUBLIC_KEY_OID}'"
83 )
84
85 # See 13.3. EdDSA Point Format
86 public_key_len = gpg_util.get_mpi_length(data[ptr : ptr + 2])
87 ptr += 2
88
89 if public_key_len != ED25519_PUBLIC_KEY_LENGTH:
90 raise PacketParsingError(
91 f"bad ed25519 MPI length '{public_key_len}', "
92 f"expected {ED25519_PUBLIC_KEY_LENGTH}'"
93 )
94
95 public_key_prefix = data[ptr]
96 ptr += 1
97
98 if public_key_prefix != ED25519_PUBLIC_KEY_PREFIX:
99 raise PacketParsingError(
100 f"bad ed25519 MPI prefix '{public_key_prefix}', "
101 f"expected '{ED25519_PUBLIC_KEY_PREFIX}'"
102 )
103
104 public_key = data[ptr : ptr + public_key_len - 1]
105
106 return {"q": binascii.hexlify(public_key).decode("ascii")}
107
108
109def get_signature_params(data):
110 """
111 <Purpose>
112 Parse algorithm-specific fields for EdDSA signatures.
113
114 See RFC4880-bis8 section 5.2.3. Version 4 and 5 Signature Packet Formats
115 for more details.
116
117 <Arguments>
118 data:
119 The EdDSA signature data AFTER the two-octet field holding the
120 left 16 bits of the signed hash value.
121
122 <Exceptions>
123 IndexError if the signature data is malformed.
124
125 <Side Effects>
126 None.
127
128 <Returns>
129 The concatenation of the parsed MPI R and S values of the EdDSA signature,
130 i.e. ENC(R) || ENC(S) (see RFC8032 3.4 Verify).
131
132 """
133 ptr = 0
134 r_length = gpg_util.get_mpi_length(data[ptr : ptr + 2])
135
136 ptr += 2
137 r = data[ptr : ptr + r_length]
138 ptr += r_length
139
140 s_length = gpg_util.get_mpi_length(data[ptr : ptr + 2])
141 ptr += 2
142 s = data[ptr : ptr + s_length]
143
144 # Left-zero-pad 'r' and 's' values that are shorter than required by RFC 8032
145 # (5.1.6.), to make up for omitted leading zeros in RFC 4880 (3.2.) MPIs.
146 # This is especially important for 's', which is little-endian.
147 r = r.rjust(ED25519_SIG_LENGTH // 2, b"\x00")
148 s = s.rjust(ED25519_SIG_LENGTH // 2, b"\x00")
149
150 return r + s
151
152
153def create_pubkey(pubkey_info):
154 """
155 <Purpose>
156 Create and return an Ed25519PublicKey object from the passed pubkey_info
157 using pyca/cryptography.
158
159 <Arguments>
160 pubkey_info:
161 The ED25519 public key dict.
162
163 <Exceptions>
164
165 securesystemslib.exceptions.UnsupportedLibraryError if
166 the cryptography module is unavailable
167
168 <Returns>
169 A cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey based
170 on the passed pubkey_info.
171
172 """
173 if not CRYPTO: # pragma: no cover
174 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG)
175
176 public_bytes = binascii.unhexlify(pubkey_info["keyval"]["public"]["q"])
177 public_key = pyca_ed25519.Ed25519PublicKey.from_public_bytes(public_bytes)
178
179 return public_key
180
181
182def verify_signature(signature_object, pubkey_info, content, hash_algorithm_id):
183 """
184 <Purpose>
185 Verify the passed signature against the passed content with the passed
186 ED25519 public key using pyca/cryptography.
187
188 <Arguments>
189 signature_object:
190 A signature dict.
191
192 pubkey_info:
193 A DSA public key dict.
194
195 hash_algorithm_id:
196 one of SHA1, SHA256, SHA512 (see securesystemslib._gpg.constants)
197 used to verify the signature
198 NOTE: Overrides any hash algorithm specification in "pubkey_info"'s
199 "hashes" or "method" fields.
200
201 content:
202 The signed bytes against which the signature is verified
203
204 <Exceptions>
205 securesystemslib.exceptions.UnsupportedLibraryError if:
206 the cryptography module is unavailable
207
208 ValueError:
209 if the passed hash_algorithm_id is not supported (see
210 securesystemslib._gpg.util.get_hashing_class)
211
212 <Returns>
213 True if signature verification passes and False otherwise.
214
215 """
216 if not CRYPTO: # pragma: no cover
217 raise exceptions.UnsupportedLibraryError(NO_CRYPTO_MSG)
218
219 hasher = gpg_util.get_hashing_class(hash_algorithm_id)
220
221 pubkey_object = create_pubkey(pubkey_info)
222
223 # See RFC4880-bis8 14.8. EdDSA and 5.2.4 "Computing Signatures"
224 digest = gpg_util.hash_object(
225 binascii.unhexlify(signature_object["other_headers"]), hasher(), content
226 )
227
228 try:
229 pubkey_object.verify(binascii.unhexlify(signature_object["signature"]), digest)
230 return True
231
232 except InvalidSignature:
233 return False