Lines
0 %
Functions
Branches
100 %
use anyhow::anyhow;
use base64::{engine::general_purpose::STANDARD, Engine};
use chrono::{DateTime, Utc};
use review_harvest::{
gpg::{check_signature, load_keys, PgpSignature},
urn::blob_to_uri,
};
use sequoia_openpgp::{
packet::{signature::Signature4, Signature},
Cert, Fingerprint, KeyHandle, KeyID,
use sophia::{
api::{
graph::MutableGraph,
ns::xsd::{base64Binary, hexBinary},
ns::Namespace,
serializer::{Stringifier, TripleSerializer},
term::{SimpleTerm, Term},
},
inmem::graph::LightGraph,
iri::{Iri, IriRef},
turtle::serializer::nt::NtSerializer,
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
type Result<T> = anyhow::Result<T>;
/// check that the secret/private key is present in the repository
fn check_key(key: &str, gpgdir: &Path) -> Result<bool> {
let gpgdir = gpgdir.to_str().unwrap();
let out = Command::new("gpg")
.args(["--batch", "--homedir", gpgdir, "--list-secret-keys", key])
.output()?;
Ok(out.status.success())
}
fn sign_bytearray(key: &str, gpgdir: &Path, data: &[u8]) -> Result<Vec<u8>> {
let mut child = Command::new("gpg")
.args([
"--batch",
"--homedir",
gpgdir,
"--local-user",
key,
"--detach-sig",
"--digest-algo",
"SHA512",
"--output",
"-",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = if let Some(stdin) = child.stdin.take() {
stdin
} else {
return Err(anyhow!("No stdin"));
stdin.write_all(data)?;
drop(stdin);
let output = child.wait_with_output()?;
if !output.status.success() {
return Err(anyhow!(
"Could not sign bytes: {}",
&String::from_utf8_lossy(&output.stderr)
));
Ok(output.stdout)
// encode a byte array to base64 binary
// https://www.w3.org/TR/xmlschema-2/#base64Binary
// The encoding is done in the canonical form Canonical-base64Binary.
// This means that there is no whitespace and the value is padded.
// So the length is always a multiple of four.
fn blob_to_xsd_base64_binary(data: &[u8]) -> String {
// STANDARD refers to RFC 3548, XSD refers to RFC 2045.
// RFC 3548 uses the same alphabet for the base64 encoding, but disallows
// whitespace unless explicilty allowed.
// STANDARD uses padding as required by
let base64 = STANDARD.encode(data);
assert!(base64.len() % 4 == 0);
base64
fn blob_to_rdf_base64_literal(data: &[u8]) -> SimpleTerm {
let base64 = blob_to_xsd_base64_binary(data);
SimpleTerm::LiteralDatatype(base64.into(), base64Binary.iri().unwrap())
// encode a byte array to uppercase hex
// Uppercase is the canonical mapping for hex in xsd.
// It is also the convention for public key fingerprints
fn blob_to_xsd_hex_binary(data: &[u8]) -> String {
hex::encode_upper(data)
fn blob_to_rdf_hex_literal(data: &[u8]) -> SimpleTerm {
let hex = blob_to_xsd_hex_binary(data);
SimpleTerm::LiteralDatatype(hex.into(), hexBinary.iri().unwrap())
// write an rdf file with a signature
// returns the iri of the signature
fn write_rdf_signature(
data: &[u8],
signature: &[u8],
output: &Path,
) -> Result<(LightGraph, String)> {
let mut graph = LightGraph::new();
let local = Namespace::new("")?;
let harvest = Namespace::new("https://example.com/harvest#")?;
let subject = local.get("#signature")?;
let data_uri = blob_to_uri(data);
let data_uri = IriRef::new(data_uri)?;
let hascontent = blob_to_rdf_base64_literal(signature);
graph.insert(subject, harvest.get("on_payload")?, data_uri)?;
graph.insert(subject, harvest.get("has_content")?, &hascontent)?;
let mut nt_stringifier = NtSerializer::new_stringifier();
let rdf = nt_stringifier.serialize_graph(&graph)?.as_str();
std::fs::write(output, rdf)?;
println!("wrote {}", output.display());
let signature_iri = blob_to_uri(rdf.as_bytes()) + "#signature";
Ok((graph, signature_iri))
// Parse the detached signature into triples
fn detached_signature_to_triples<G: MutableGraph>(
certs: &[Cert],
payload: &[u8],
graph: &mut G,
signature_iri: String,
) -> Result<()>
where
<G as MutableGraph>::MutationError: Send + Sync,
{
let signature_iri_term = Iri::new(signature_iri)?;
let signature = PgpSignature {
signature: signature.to_vec(),
payload: payload.to_vec(),
let validated_signature = check_signature(certs, &signature)?;
for key in validated_signature.certificates {
match key.0 {
// A 20 byte SHA-1 hash of the public key packet as defined in the RFC.
KeyHandle::Fingerprint(Fingerprint::V4(fp)) => {
println!("fingerprint {}", hex::encode(fp));
let fp = blob_to_rdf_hex_literal(&fp);
graph.insert(&signature_iri_term, harvest.get("signed_by")?, fp)?;
// A v5 OpenPGP fingerprint.
KeyHandle::Fingerprint(Fingerprint::V5(fp)) => {
// Lower 8 byte SHA-1 hash.
KeyHandle::KeyID(KeyID::V4(fp)) => {
_ => {}
match validated_signature.signature {
Signature::V3(signature) => {
add_signature(graph, &signature);
Signature::V4(signature) => {
let _ = graph.triples();
Ok(())
fn iso8601(st: std::time::SystemTime) -> String {
let dt: DateTime<Utc> = st.into();
format!("{}", dt.format("%+"))
// formats like "2001-07-08T00:34:60.026490+09:30"
fn add_signature<G: MutableGraph>(_graph: &mut G, signature: &Signature4) {
if let Some(digest) = signature.computed_digest() {
let base64 = STANDARD.encode(digest);
println!("digest: {}", base64);
println!("digest: {}", hex::encode(digest));
if let Some(creation_time) = signature.signature_creation_time() {
println!("creation time: {}", iso8601(creation_time));
if let Some(validity_period) = signature.signature_validity_period() {
println!("validity period: {:?}", validity_period);
if let Some(expiration_time) = signature.signature_expiration_time() {
println!("expiration time: {}", iso8601(expiration_time));
if let Some(key_validity_period) = signature.key_validity_period() {
println!("key validity period: {:?}", key_validity_period);
if let Some(exportable_certification) = signature.exportable_certification() {
println!("exportable certification: {:?}", exportable_certification);
println!("type: {}", signature.typ());
println!("key algo: {}", signature.pk_algo());
println!("hash algo: {}", signature.hash_algo());
const HELP: &str = "\
App
USAGE:
sign [OPTIONS]
FLAGS:
-h, --help Prints help information
OPTIONS:
--keyid ID The identifier of the key with which the file should be signed
--gpgdir PATH The directory with the gpg files
--file PATH The file from which should be signed
--output PATH The file to write the signature to
--rdf-output PATH The rdf file into which to write the signature
";
#[derive(Debug)]
struct Args {
keyid: String,
gpgdir: PathBuf,
file: PathBuf,
output: Option<PathBuf>,
rdfoutput: Option<PathBuf>,
fn parse_path(s: &std::ffi::OsStr) -> std::result::Result<std::path::PathBuf, &'static str> {
Ok(s.into())
fn parse_args() -> std::result::Result<Args, pico_args::Error> {
let mut pargs = pico_args::Arguments::from_env();
// Help has a higher priority and should be handled separately.
if pargs.contains(["-h", "--help"]) {
print!("{}", HELP);
std::process::exit(0);
let args = Args {
keyid: pargs.value_from_str("--keyid")?,
gpgdir: pargs.value_from_os_str("--gpgdir", parse_path)?,
file: pargs.value_from_os_str("--file", parse_path)?,
output: pargs.opt_value_from_os_str("--output", parse_path)?,
rdfoutput: pargs.opt_value_from_os_str("--rdf-output", parse_path)?,
let remaining_arguments = pargs.finish();
if !remaining_arguments.is_empty() {
eprintln!("There are unused arguments:");
for remaining in remaining_arguments {
if let Ok(remaining) = remaining.into_string() {
eprintln!("Unused argument: {}", remaining);
std::process::exit(1);
Ok(args)
fn main() -> Result<()> {
let args = match parse_args() {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {}.", e);
let key_ok = check_key(&args.keyid, &args.gpgdir)?;
if !key_ok {
return Err(anyhow!("'{}' is not a known valid key.", args.keyid));
let file_content = std::fs::read(&args.file)?;
let detached_signature = sign_bytearray(&args.keyid, &args.gpgdir, &file_content)?;
if let Some(output) = &args.output {
std::fs::write(output, &detached_signature)?;
if let Some(rdfoutput) = &args.rdfoutput {
let (mut graph, signature_iri) =
write_rdf_signature(&file_content, &detached_signature, rdfoutput)?;
let certs = load_keys(&args.gpgdir)?;
detached_signature_to_triples(
&certs,
&detached_signature,
&file_content,
&mut graph,
signature_iri,
)?;