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
fn check_key(key: &str, gpgdir: &Path) -> Result<bool> {
32
    let gpgdir = gpgdir.to_str().unwrap();
33
    let out = Command::new("gpg")
34
        .args(["--batch", "--homedir", gpgdir, "--list-secret-keys", key])
35
        .output()?;
36
    Ok(out.status.success())
37
}
38

            
39
fn sign_bytearray(key: &str, gpgdir: &Path, data: &[u8]) -> Result<Vec<u8>> {
40
    let gpgdir = gpgdir.to_str().unwrap();
41
    let mut child = Command::new("gpg")
42
        .args([
43
            "--batch",
44
            "--homedir",
45
            gpgdir,
46
            "--local-user",
47
            key,
48
            "--detach-sig",
49
            "--digest-algo",
50
            "SHA512",
51
            "--output",
52
            "-",
53
        ])
54
        .stdin(Stdio::piped())
55
        .stdout(Stdio::piped())
56
        .stderr(Stdio::piped())
57
        .spawn()?;
58
    let mut stdin = if let Some(stdin) = child.stdin.take() {
59
        stdin
60
    } else {
61
        return Err(anyhow!("No stdin"));
62
    };
63
    stdin.write_all(data)?;
64
    drop(stdin);
65
    let output = child.wait_with_output()?;
66
    if !output.status.success() {
67
        return Err(anyhow!(
68
            "Could not sign bytes: {}",
69
            &String::from_utf8_lossy(&output.stderr)
70
        ));
71
    }
72
    Ok(output.stdout)
73
}
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
fn blob_to_xsd_base64_binary(data: &[u8]) -> String {
81
    // STANDARD refers to RFC 3548, XSD refers to RFC 2045.
82
    // RFC 3548 uses the same alphabet for the base64 encoding, but disallows
83
    // whitespace unless explicilty allowed.
84
    // STANDARD uses padding as required by
85
    let base64 = STANDARD.encode(data);
86
    assert!(base64.len() % 4 == 0);
87
    base64
88
}
89

            
90
fn blob_to_rdf_base64_literal(data: &[u8]) -> SimpleTerm {
91
    let base64 = blob_to_xsd_base64_binary(data);
92
    SimpleTerm::LiteralDatatype(base64.into(), base64Binary.iri().unwrap())
93
}
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
fn blob_to_xsd_hex_binary(data: &[u8]) -> String {
99
    hex::encode_upper(data)
100
}
101

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

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

            
131
// Parse the detached signature into triples
132
fn detached_signature_to_triples<G: MutableGraph>(
133
    certs: &[Cert],
134
    signature: &[u8],
135
    payload: &[u8],
136
    graph: &mut G,
137
    signature_iri: String,
138
) -> Result<()>
139
where
140
    <G as MutableGraph>::MutationError: Send + Sync,
141
{
142
    let harvest = Namespace::new("https://example.com/harvest#")?;
143
    let signature_iri_term = Iri::new(signature_iri)?;
144
    let signature = PgpSignature {
145
        signature: signature.to_vec(),
146
        payload: payload.to_vec(),
147
    };
148
    let validated_signature = check_signature(certs, &signature)?;
149
    for key in validated_signature.certificates {
150
        match key.0 {
151
            // A 20 byte SHA-1 hash of the public key packet as defined in the RFC.
152
            KeyHandle::Fingerprint(Fingerprint::V4(fp)) => {
153
                println!("fingerprint {}", hex::encode(fp));
154
                let fp = blob_to_rdf_hex_literal(&fp);
155
                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
    match validated_signature.signature {
173
        Signature::V3(signature) => {
174
            add_signature(graph, &signature);
175
        }
176
        Signature::V4(signature) => {
177
            add_signature(graph, &signature);
178
        }
179
        _ => {}
180
    }
181
    let _ = graph.triples();
182
    Ok(())
183
}
184

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

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

            
217
const HELP: &str = "\
218
App
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
fn parse_path(s: &std::ffi::OsStr) -> std::result::Result<std::path::PathBuf, &'static str> {
244
    Ok(s.into())
245
}
246

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

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

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

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

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