This repository has been archived by the owner on Aug 17, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathsignatures.nim
261 lines (227 loc) · 7.73 KB
/
signatures.nim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#
# Nimble package signing
#
# Copyright 2016 Federico Ceratto <federico.ceratto@gmail.com>
# Released under GPLv3 License, see LICENSE file
#
# Core developer keys -s-> roster.json --> buildbot/endorsed keys --> signed releases
# Owners -sign-> signed packages.json block -id-> signed package
# Nimble process:
# 1)
# Update roster.json
# load and verify roster
# load and verify packages.json
#
# a verify binary package .asc file using key from roster
# 2)
# load and verify packages.json block using owner key
#
#
# Pkg owner workflow:
# Sign packages item including authorized keys
# Sign Git tags
# Sign tarball?
import std/[
base64,
httpclient,
json,
os,
osproc,
streams,
strutils,
sequtils,
tables,
times
]
import tempfile
from sequtils import toSeq
from algorithm import sorted
const
gpg_path = "/usr/bin/gpg"
core_accepted_keys = @[
"0x4E6CA40C9E4B23FB", # araq
"0xC9087E57971FB655", # dom
]
proc to_s_ugly(result: var string, node: JsonNode) =
## Converts `node` to its JSON Representation, without
## regard for human readability. Meant to improve ``$`` string
## conversion performance.
##
## JSON representation is stored in the passed `result`
##
## This provides higher efficiency than the ``pretty`` procedure as it
## does **not** attempt to format the resulting JSON to make it human readable.
var comma = false
case node.kind:
of JArray:
result.add "["
for child in node.elems:
if comma: result.add ","
else: comma = true
result.to_s_ugly child
result.add "]"
of JObject:
result.add "{"
for n, key_value in toSeq(pairs(node.fields)).sorted(system.cmp):
let (key, value) = key_value
if comma: result.add ","
else: comma = true
result.add key.escapeJson()
result.add ":"
result.to_s_ugly value
result.add "}"
of JString:
result.add node.str.escapeJson()
of JInt:
result.add($node.num)
of JFloat:
result.add($node.fnum)
of JBool:
result.add(if node.bval: "true" else: "false")
of JNull:
result.add "null"
proc serialize(node: JsonNode): string =
## Serialize node
## Keys are sorted lexicographically, the indentation is two whitespaces,
## there are no newlines and spaces at the beginning and the end.
result = newStringOfCap(node.len shl 1)
to_s_ugly(result, node)
import strtabs
proc generate_gpg_signature*(node: JsonNode, key: string): string =
## Generate GPG signature for a JSON node
## The signature is generated by GPG-signing the output of
## pretty(indent=2)
## Keys are sorted lexicographically, the indentation is two whitespaces,
## there are no newlines and spaces at the beginning and the end.
let
tmp_dir = mkdtemp()
tmp_clearfn = tmp_dir / "cleartext"
tmp_signature = tmp_dir / "signature"
tmp_clearfn.writeFile(node.serialize)
let tty = execProcess("/usr/bin/tty")
let local_user =
if key.len == 0: ""
else: "--local-user $#" % [key]
let cmd = "$# --detach-sign --output $# $# $#" % [gpg_path,
tmp_signature, local_user, tmp_clearfn]
assert dir_exists(tmp_dir)
assert file_exists(tmp_clearfn)
let env = newStringTable("GPG_TTY", tty, modeCaseInsensitive)
echo env # FIXME use env?
var cmd_out = ""
try:
cmd_out = execProcess(cmd)#, env=env)
result = tmp_signature.readFile().encode()
if key.len == 0:
doAssert cmd_out.contains("default secret key for signing"), "NO DE"
doAssert cmd_out.contains("signing failed") == false, "output "
tmp_dir.removeDir()
except:
echo "signing failure: ", getCurrentExceptionMsg()
echo "gpg command: ", cmd
echo $dir_exists(tmp_dir)
echo "## gpg output ##"
echo cmd_out
echo "## end ouf output ##"
#tmp_dir.removeDir()
raise newException(Exception, "Failed to sign:\n$#" % cmd_out)
proc embed_gpg_signature*(node: JsonNode, key: string) =
## Generate a GPG signature for a JSON node
## The "signatures" key is emptied during the signing
## The new signature is added to "signatures" as (key_id, signature)
let tmpnode = copy(node)
if tmpnode.has_key("signatures"):
tmpnode.delete("signatures")
let sig = generate_gpg_signature(tmpnode, key)
if not node.has_key("signatures"):
node.add("signatures", newJArray())
node["signatures"].add newJString(sig)
proc verify_gpg_signature*(node: JsonNode, signature: string): string =
## Verify a GPG signature on a JSON node
## Returns: key id
let
tmp_dir = mkdtemp()
tmp_clearfn = tmp_dir / "cleartext"
tmp_signature = tmp_dir / "signature"
tmp_clearfn.writeFile(node.serialize)
tmp_signature.writeFile(signature.decode())
let gpg = startProcess(gpg_path, args = ["--verify", tmp_signature,
tmp_clearfn])
let exit_code = gpg.waitForExit(timeout=30)
result = gpg.outputStream.readAll()
tmp_dir.removeDir()
if exit_code != 0:
raise newException(Exception, "Bad signature:\n$#" % result)
proc verify_gpg_signature_is_allowed*(node: JsonNode, signature: string,
accepted_keys: seq[string] = @[]): string =
## Verify that a GPG signature is valid and the key belongs to the set
## of accepted keys
let gpg_out = verify_gpg_signature(node, signature)
var signing_key = ""
for line in gpg_out.splitLines:
if line.contains("using ") and line.contains(" key 0x"):
let p = line.find(" key 0x") + 5
signing_key = line[p..p+18].toUpperAscii
for accepted_k in accepted_keys:
if signing_key == accepted_k.toUpperAscii:
return signing_key
raise newException(Exception, "$# is not an accepted key" % signing_key)
proc verify_enough_allowed_gpg_signatures*(node: JsonNode,
accepted_keys: seq[string] = @[], threshold: int) =
## Verify that a JSON node is signed by enough accepted keys
## Other keys are verified and then ignored
if not node.has_key("signatures"):
raise newException(Exception, "Missing signatures field")
let tmpnode = copy(node)
tmpnode.delete("signatures")
var validated_cnt = 0
for sig in node["signatures"]:
try:
discard tmpnode.verify_gpg_signature_is_allowed(
sig.str, accepted_keys)
validated_cnt.inc
if validated_cnt == threshold:
return
except:
# either the signature is not valid or from a non-allowed key
discard
raise newException(Exception, "Not enough allowed signatures $# $#" % [$validated_cnt, $threshold])
proc load_and_verify_roster*(fname = "roster.json",
accepted_keys=core_accepted_keys, required_sigs_num=2): JsonNode =
## Load and verify roster
result = fname.readFile.parseJson()
stdout.write "Verifying roster... "
result.verify_enough_allowed_gpg_signatures(accepted_keys,
required_sigs_num)
echo "[OK]"
proc verify_package_metadata*(node: JsonNode) =
## Verify owner[s] signature[s] on packages.json item
stdout.write "Verifying metadata... "
let
owners_keys = node["owner_keys"].mapIt(it.str)
sigs = node["signatures"].mapIt(it.str)
for sig in sigs:
discard node.verify_gpg_signature_is_allowed(sig, owners_keys)
echo "[OK]"
return
raise newException(Exception, "No valid owner signature found")
proc download_file*(url, fname: string, check_modified_time=true,
timeout=30): bool =
## Download file, if needed
if fileExists(fname) and check_modified_time:
let
creation_time = fname.getFileInfo.creationTime
tstamp = creation_time.format("ddd, dd MMM yyyy hh:mm:ss") & " GMT"
# If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
let hc = newHttpClient()
hc.headers["If-Modified-Since:"] = tstamp
#hc.timeout = timeout
let resp = hc.request(url)
if resp.status == "304":
return false
writeFile(fname, resp.body)
return true
else:
# echo "Fetching ", url
writeFile(fname, newHttpClient().getContent(url))
return true