1
use anyhow::anyhow;
2
use base64::{engine::general_purpose::STANDARD, Engine};
3
use chrono::{DateTime, Utc};
4
use review_harvest::{
5
    gpg::{check_signature, load_keys, PgpSignature},
6
    urn::blob_to_uri,
7
};
8
use sequoia_openpgp::{
9
    packet::{signature::Signature4, Signature},
10
    Cert, Fingerprint, KeyHandle, KeyID,
11
};
12
use sophia::{
13
    api::{
14
        graph::MutableGraph,
15
        ns::xsd::{base64Binary, hexBinary},
16
        ns::Namespace,
17
        serializer::{Stringifier, TripleSerializer},
18
        term::{SimpleTerm, Term},
19
    },
20
    inmem::graph::LightGraph,
21
    iri::{Iri, IriRef},
22
    turtle::serializer::nt::NtSerializer,
23
};
24
use std::io::Write;
25
use std::path::{Path, PathBuf};
26
use std::process::{Command, Stdio};
27

            
28
type Result<T> = anyhow::Result<T>;
29

            
30
/// check that the secret/private key is present in the repository
31
2
fn check_key(key: &str, gpgdir: &Path) -> Result<bool> {
32
2
    let gpgdir = gpgdir.to_str().unwrap();
33
2
    let out = Command::new("gpg")
34
2
        .args(["--batch", "--homedir", gpgdir, "--list-secret-keys", key])
35
2
        .output()?;
36
2
    Ok(out.status.success())
37
2
}
38

            
39
2
fn sign_bytearray(key: &str, gpgdir: &Path, data: &[u8]) -> Result<Vec<u8>> {
40
2
    let gpgdir = gpgdir.to_str().unwrap();
41
2
    let mut child = Command::new("gpg")
42
2
        .args([
43
2
            "--batch",
44
2
            "--homedir",
45
2
            gpgdir,
46
2
            "--local-user",
47
2
            key,
48
2
            "--detach-sig",
49
2
            "--digest-algo",
50
2
            "SHA512",
51
2
            "--output",
52
2
            "-",
53
2
        ])
54
2
        .stdin(Stdio::piped())
55
2
        .stdout(Stdio::piped())
56
2
        .stderr(Stdio::piped())
57
2
        .spawn()?;
58
2
    let mut stdin = if let Some(stdin) = child.stdin.take() {
59
2
        stdin
60
    } else {
61
        return Err(anyhow!("No stdin"));
62
    };
63
2
    stdin.write_all(data)?;
64
2
    drop(stdin);
65
2
    let output = child.wait_with_output()?;
66
2
    if !output.status.success() {
67
        return Err(anyhow!(
68
            "Could not sign bytes: {}",
69
            &String::from_utf8_lossy(&output.stderr)
70
        ));
71
2
    }
72
2
    Ok(output.stdout)
73
2
}
74

            
75
// encode a byte array to base64 binary
76
// https://www.w3.org/TR/xmlschema-2/#base64Binary
77
// The encoding is done in the canonical form Canonical-base64Binary.
78
// This means that there is no whitespace and the value is padded.
79
// So the length is always a multiple of four.
80
2
fn blob_to_xsd_base64_binary(data: &[u8]) -> String {
81
2
    // STANDARD refers to RFC 3548, XSD refers to RFC 2045.
82
2
    // RFC 3548 uses the same alphabet for the base64 encoding, but disallows
83
2
    // whitespace unless explicilty allowed.
84
2
    // STANDARD uses padding as required by
85
2
    let base64 = STANDARD.encode(data);
86
2
    assert!(base64.len() % 4 == 0);
87
2
    base64
88
2
}
89

            
90
2
fn blob_to_rdf_base64_literal(data: &[u8]) -> SimpleTerm {
91
2
    let base64 = blob_to_xsd_base64_binary(data);
92
2
    SimpleTerm::LiteralDatatype(base64.into(), base64Binary.iri().unwrap())
93
2
}
94

            
95
// encode a byte array to uppercase hex
96
// Uppercase is the canonical mapping for hex in xsd.
97
// It is also the convention for public key fingerprints
98
2
fn blob_to_xsd_hex_binary(data: &[u8]) -> String {
99
2
    hex::encode_upper(data)
100
2
}
101

            
102
2
fn blob_to_rdf_hex_literal(data: &[u8]) -> SimpleTerm {
103
2
    let hex = blob_to_xsd_hex_binary(data);
104
2
    SimpleTerm::LiteralDatatype(hex.into(), hexBinary.iri().unwrap())
105
2
}
106

            
107
// write an rdf file with a signature
108
// returns the iri of the signature
109
2
fn write_rdf_signature(
110
2
    data: &[u8],
111
2
    signature: &[u8],
112
2
    output: &Path,
113
2
) -> Result<(LightGraph, String)> {
114
2
    let mut graph = LightGraph::new();
115
2
    let local = Namespace::new("")?;
116
2
    let harvest = Namespace::new("https://example.com/harvest#")?;
117
2
    let subject = local.get("#signature")?;
118
2
    let data_uri = blob_to_uri(data);
119
2
    let data_uri = IriRef::new(data_uri)?;
120
2
    let hascontent = blob_to_rdf_base64_literal(signature);
121
2
    graph.insert(subject, harvest.get("on_payload")?, data_uri)?;
122
2
    graph.insert(subject, harvest.get("has_content")?, &hascontent)?;
123
2
    let mut nt_stringifier = NtSerializer::new_stringifier();
124
2
    let rdf = nt_stringifier.serialize_graph(&graph)?.as_str();
125
2
    std::fs::write(output, rdf)?;
126
2
    println!("wrote {}", output.display());
127
2
    let signature_iri = blob_to_uri(rdf.as_bytes()) + "#signature";
128
2
    Ok((graph, signature_iri))
129
2
}
130

            
131
// Parse the detached signature into triples
132
2
fn detached_signature_to_triples<G: MutableGraph>(
133
2
    certs: &[Cert],
134
2
    signature: &[u8],
135
2
    payload: &[u8],
136
2
    graph: &mut G,
137
2
    signature_iri: String,
138
2
) -> Result<()>
139
2
where
140
2
    <G as MutableGraph>::MutationError: Send + Sync,
141
2
{
142
2
    let harvest = Namespace::new("https://example.com/harvest#")?;
143
2
    let signature_iri_term = Iri::new(signature_iri)?;
144
2
    let signature = PgpSignature {
145
2
        signature: signature.to_vec(),
146
2
        payload: payload.to_vec(),
147
2
    };
148
2
    let validated_signature = check_signature(certs, &signature)?;
149
4
    for key in validated_signature.certificates {
150
2
        match key.0 {
151
            // A 20 byte SHA-1 hash of the public key packet as defined in the RFC.
152
2
            KeyHandle::Fingerprint(Fingerprint::V4(fp)) => {
153
2
                println!("fingerprint {}", hex::encode(fp));
154
2
                let fp = blob_to_rdf_hex_literal(&fp);
155
2
                graph.insert(&signature_iri_term, harvest.get("signed_by")?, fp)?;
156
            }
157
            // A v5 OpenPGP fingerprint.
158
            KeyHandle::Fingerprint(Fingerprint::V5(fp)) => {
159
                println!("fingerprint {}", hex::encode(fp));
160
                let fp = blob_to_rdf_hex_literal(&fp);
161
                graph.insert(&signature_iri_term, harvest.get("signed_by")?, fp)?;
162
            }
163
            // Lower 8 byte SHA-1 hash.
164
            KeyHandle::KeyID(KeyID::V4(fp)) => {
165
                println!("fingerprint {}", hex::encode(fp));
166
                let fp = blob_to_rdf_hex_literal(&fp);
167
                graph.insert(&signature_iri_term, harvest.get("signed_by")?, fp)?;
168
            }
169
            _ => {}
170
        }
171
    }
172
2
    match validated_signature.signature {
173
        Signature::V3(signature) => {
174
            add_signature(graph, &signature);
175
        }
176
2
        Signature::V4(signature) => {
177
2
            add_signature(graph, &signature);
178
2
        }
179
        _ => {}
180
    }
181
2
    let _ = graph.triples();
182
2
    Ok(())
183
2
}
184

            
185
2
fn iso8601(st: std::time::SystemTime) -> String {
186
2
    let dt: DateTime<Utc> = st.into();
187
2
    format!("{}", dt.format("%+"))
188
2
    // formats like "2001-07-08T00:34:60.026490+09:30"
189
2
}
190

            
191
2
fn add_signature<G: MutableGraph>(_graph: &mut G, signature: &Signature4) {
192
2
    if let Some(digest) = signature.computed_digest() {
193
2
        let base64 = STANDARD.encode(digest);
194
2
        println!("digest: {}", base64);
195
2
        println!("digest: {}", hex::encode(digest));
196
2
    }
197
2
    if let Some(creation_time) = signature.signature_creation_time() {
198
2
        println!("creation time: {}", iso8601(creation_time));
199
2
    }
200
2
    if let Some(validity_period) = signature.signature_validity_period() {
201
        println!("validity period: {:?}", validity_period);
202
2
    }
203
2
    if let Some(expiration_time) = signature.signature_expiration_time() {
204
        println!("expiration time: {}", iso8601(expiration_time));
205
2
    }
206
2
    if let Some(key_validity_period) = signature.key_validity_period() {
207
        println!("key validity period: {:?}", key_validity_period);
208
2
    }
209
2
    if let Some(exportable_certification) = signature.exportable_certification() {
210
        println!("exportable certification: {:?}", exportable_certification);
211
2
    }
212
2
    println!("type: {}", signature.typ());
213
2
    println!("key algo: {}", signature.pk_algo());
214
2
    println!("hash algo: {}", signature.hash_algo());
215
2
}
216

            
217
const HELP: &str = "\
218
sign
219

            
220
USAGE:
221
  sign [OPTIONS]
222

            
223
FLAGS:
224
  -h, --help    Prints help information
225

            
226
OPTIONS:
227
  --keyid ID        The identifier of the key with which the file should be signed
228
  --gpgdir PATH     The directory with the gpg files
229
  --file PATH       The file from which should be signed
230
  --output PATH     The file to write the signature to
231
  --rdf-output PATH The rdf file into which to write the signature
232
";
233

            
234
#[derive(Debug)]
235
struct Args {
236
    keyid: String,
237
    gpgdir: PathBuf,
238
    file: PathBuf,
239
    output: Option<PathBuf>,
240
    rdfoutput: Option<PathBuf>,
241
}
242

            
243
12
fn parse_path(s: &std::ffi::OsStr) -> std::result::Result<std::path::PathBuf, &'static str> {
244
12
    Ok(s.into())
245
12
}
246

            
247
8
fn parse_args() -> std::result::Result<Args, pico_args::Error> {
248
8
    let mut pargs = pico_args::Arguments::from_env();
249
8

            
250
8
    // Help has a higher priority and should be handled separately.
251
8
    if pargs.contains(["-h", "--help"]) {
252
2
        print!("{}", HELP);
253
2
        std::process::exit(0);
254
6
    }
255
4
    let args = Args {
256
6
        keyid: pargs.value_from_str("--keyid")?,
257
4
        gpgdir: pargs.value_from_os_str("--gpgdir", parse_path)?,
258
4
        file: pargs.value_from_os_str("--file", parse_path)?,
259
4
        output: pargs.opt_value_from_os_str("--output", parse_path)?,
260
4
        rdfoutput: pargs.opt_value_from_os_str("--rdf-output", parse_path)?,
261
    };
262
4
    let remaining_arguments = pargs.finish();
263
4
    if !remaining_arguments.is_empty() {
264
2
        eprintln!("There are unused arguments:");
265
4
        for remaining in remaining_arguments {
266
2
            if let Ok(remaining) = remaining.into_string() {
267
2
                eprintln!("Unused argument: {}", remaining);
268
2
            }
269
        }
270
2
        print!("{}", HELP);
271
2
        std::process::exit(1);
272
2
    }
273
2
    Ok(args)
274
4
}
275

            
276
8
fn main() -> Result<()> {
277
8
    let args = match parse_args() {
278
6
        Ok(v) => v,
279
2
        Err(e) => {
280
2
            eprintln!("Error: {}.", e);
281
2
            std::process::exit(1);
282
        }
283
    };
284

            
285
6
    let key_ok = check_key(&args.keyid, &args.gpgdir)?;
286
6
    if !key_ok {
287
        return Err(anyhow!("'{}' is not a known valid key.", args.keyid));
288
6
    }
289

            
290
6
    let file_content = std::fs::read(&args.file)?;
291
6
    let detached_signature = sign_bytearray(&args.keyid, &args.gpgdir, &file_content)?;
292
6
    if let Some(output) = &args.output {
293
6
        std::fs::write(output, &detached_signature)?;
294
    }
295
6
    if let Some(rdfoutput) = &args.rdfoutput {
296
6
        let (mut graph, signature_iri) =
297
6
            write_rdf_signature(&file_content, &detached_signature, rdfoutput)?;
298
6
        let certs = load_keys(&args.gpgdir)?;
299
6
        detached_signature_to_triples(
300
6
            &certs,
301
6
            &detached_signature,
302
6
            &file_content,
303
6
            &mut graph,
304
6
            signature_iri,
305
6
        )?;
306
    }
307
2
    Ok(())
308
2
}