Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ jobs:
if: matrix.target != 'x86_64-apple-darwin' && !matrix.cross
run: cargo run --release --target ${{ matrix.target }} -- --help

# Post-build check of the module as a PKCS#11 host sees it, no credentials
# needed: it loads, exposes the ssign token and the SHA-256 RSA mechanisms.
- name: Smoke test the PKCS#11 module
if: matrix.lint
run: |
sudo apt-get update -qq && sudo apt-get install -y opensc
module=target/${{ matrix.target }}/release/libssign_pkcs11.so
pkcs11-tool --module "$module" -L | tee slots.txt
grep -q "token label *: ssign" slots.txt
pkcs11-tool --module "$module" --list-mechanisms | tee mechanisms.txt
grep -q "^ *SHA256-RSA-PKCS, sign" mechanisms.txt
grep -q "^ *RSA-PKCS, sign" mechanisms.txt

# --- Package (binaires non signés ; la signature Apple est faite plus bas,
# dans un job dédié derrière l'environnement `signing`). ---
- name: Package (Unix)
Expand Down Expand Up @@ -149,13 +162,54 @@ jobs:
name: ssign-pkcs11-${{ matrix.label }}
path: ssign-pkcs11-${{ matrix.label }}.*

# Live checks against the Certum cloud, after the build and before anything is
# signed: PKCS#11 mechanisms, jsign (SunPKCS11), osslsigncode on every format
# and the CLI, all on the Linux binaries of this run. Both signing jobs, and so
# the release, wait for it. Behind `signing` because it reads the Certum secrets.
# Also runs on workflow_dispatch, to check a branch without tagging.
pre-sign-checks:
needs: build
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
environment: signing
env:
JSIGN_VERSION: "7.5"
JSIGN_SHA256: 602a51c3545a6dc4fb99bd2ea7152b26d1345916d0c93ddfbd5936cb735af91c
steps:
- uses: actions/checkout@v7

- name: Download the Linux CLI and module
uses: actions/download-artifact@v8
with:
pattern: ssign-*linux-x86_64
path: dl

- name: Install the test tools
run: |
sudo apt-get update -qq
sudo apt-get install -y opensc osslsigncode libengine-pkcs11-openssl
curl -fsSL -o "$RUNNER_TEMP/jsign.jar" \
"https://github.com/ebourg/jsign/releases/download/$JSIGN_VERSION/jsign-$JSIGN_VERSION.jar"
echo "$JSIGN_SHA256 $RUNNER_TEMP/jsign.jar" | sha256sum -c -
java -version

- name: Run the pre-sign checks
env:
CERTUM_EMAIL: ${{ secrets.CERTUM_EMAIL }}
CERTUM_OTP: ${{ secrets.CERTUM_OTP }}
run: |
mkdir -p bin
tar xzf dl/ssign-linux-x86_64/ssign-linux-x86_64.tar.gz -C bin
tar xzf dl/ssign-pkcs11-linux-x86_64/ssign-pkcs11-linux-x86_64.tar.gz -C bin
JSIGN_JAR="$RUNNER_TEMP/jsign.jar" tests/pre-sign.sh bin/ssign bin/libssign_pkcs11.so

# Signature + notarisation Apple. Derrière `signing` au même titre que Windows :
# une signature Developer ID engage l'identité (et donc la réputation) du
# propriétaire, elle ne doit jamais partir sans approbation explicite. Les
# secrets Apple vivent dans cet environnement — un job qui ne le déclare pas
# les lirait vides.
sign-apple:
needs: build
needs: [build, pre-sign-checks]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest
environment: signing
Expand Down Expand Up @@ -297,7 +351,7 @@ jobs:
# the owner's approval before it can read the Certum secrets and sign, and the
# release below waits for it — so a tag becomes: build → approve → signed release.
sign-windows:
needs: build
needs: [build, pre-sign-checks]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: signing
Expand Down
3 changes: 2 additions & 1 deletion ssign-core/src/authenticode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const SPC_ATTR_CONST: &[u8] = &[
0x00, 0x3e, 0x00, 0x3e, 0x00, 0x3e,
];

fn sha256(data: &[u8]) -> [u8; 32] {
/// SHA-256 of `data`.
pub fn sha256(data: &[u8]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(data);
h.finalize().into()
Expand Down
101 changes: 88 additions & 13 deletions ssign-pkcs11/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,19 +227,7 @@ impl PrivateKey for CertumKey {
}

fn sign(&self, algorithm: &SignatureAlgorithm, data: &[u8]) -> Result<Vec<u8>> {
let digest = match algorithm {
// signtool-style: the bare 32-byte digest.
SignatureAlgorithm::RsaPkcs1v15Sha256 => data.try_into().map_err(|_| {
boxed(format!("expected a 32-byte SHA-256 digest, got {}", data.len()))
})?,
// osslsigncode-style (CKM_RSA_PKCS): a DER SHA-256 DigestInfo.
SignatureAlgorithm::RsaPkcs1v15Raw => sha256_from_digestinfo(data)?,
other => {
return Err(boxed(format!(
"unsupported algorithm {other:?}; the Certum cloud cert signs SHA-256 RSA PKCS#1 v1.5 only"
)))
}
};
let digest = digest_to_sign(algorithm, data)?;
self.session
.sign_sha256(&digest)
.map_err(|e| boxed(format!("cloud signature failed: {e:#}")))
Expand All @@ -252,6 +240,22 @@ impl PrivateKey for CertumKey {
}
}

/// The 32-byte SHA-256 digest the cloud signs, from what the PKCS#11 caller
/// passed for `algorithm`.
fn digest_to_sign(algorithm: &SignatureAlgorithm, data: &[u8]) -> Result<[u8; 32]> {
match algorithm {
// CKM_SHA256_RSA_PKCS (jsign / Java SunPKCS11, pkcs11-tool): the caller
// passes the message itself, possibly over C_SignUpdate/C_SignFinal, and
// the token hashes it. Whatever its length, it is never a digest.
SignatureAlgorithm::RsaPkcs1v15Sha256 => Ok(ssign_core::authenticode::sha256(data)),
// CKM_RSA_PKCS (osslsigncode): a DER SHA-256 DigestInfo.
SignatureAlgorithm::RsaPkcs1v15Raw => sha256_from_digestinfo(data),
other => Err(boxed(format!(
"unsupported algorithm {other:?}; the Certum cloud cert signs SHA-256 RSA PKCS#1 v1.5 only"
))),
}
}

/// Peel a SHA-256 `DigestInfo` (or accept a bare digest) down to 32 bytes.
fn sha256_from_digestinfo(data: &[u8]) -> Result<[u8; 32]> {
if data.len() == SHA256_DIGESTINFO_PREFIX.len() + 32
Expand Down Expand Up @@ -317,3 +321,74 @@ pub unsafe extern "C" fn C_GetFunctionList(pp_function_list: CK_FUNCTION_LIST_PT
*pp_function_list = std::ptr::addr_of_mut!(native_pkcs11::FUNC_LIST);
CKR_OK
}

#[cfg(test)]
mod tests {
use super::*;

fn digestinfo(digest: &[u8; 32]) -> Vec<u8> {
[&SHA256_DIGESTINFO_PREFIX[..], digest].concat()
}

#[test]
fn sha256_rsa_pkcs_hashes_the_message() {
let message = b"signed attributes of an Authenticode signature";
let digest = digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Sha256, message).unwrap();
assert_eq!(digest, ssign_core::authenticode::sha256(message));
}

/// A message that happens to look like a digest or a DigestInfo is still a
/// message under CKM_SHA256_RSA_PKCS, and must be hashed.
#[test]
fn sha256_rsa_pkcs_never_guesses_from_the_length() {
let message32 = [0x42u8; 32];
let message51 = digestinfo(&[0x42u8; 32]);
for message in [&message32[..], &message51[..]] {
let digest = digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Sha256, message).unwrap();
assert_eq!(digest, ssign_core::authenticode::sha256(message));
}
}

/// Both mechanisms must end up signing the same digest for the same message,
/// or osslsigncode and jsign would produce different signatures.
#[test]
fn both_mechanisms_sign_the_same_digest() {
let message = b"same message, two mechanisms";
let via_sha256 = digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Sha256, message).unwrap();
let hashed = ssign_core::authenticode::sha256(message);
let via_raw =
digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Raw, &digestinfo(&hashed)).unwrap();
assert_eq!(via_sha256, via_raw);
}

#[test]
fn rsa_pkcs_accepts_a_bare_digest() {
let digest = [7u8; 32];
assert_eq!(
digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Raw, &digest).unwrap(),
digest
);
}

/// A SHA-384 DigestInfo must be refused, not re-hashed into a signature
/// that no verifier would accept.
#[test]
fn rsa_pkcs_rejects_other_digests() {
let sha384_digestinfo = [
&[
0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
0x02, 0x05, 0x00, 0x04, 0x30,
][..],
&[0u8; 48],
]
.concat();
assert!(digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Raw, &sha384_digestinfo).is_err());
assert!(digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Raw, b"not a digest").is_err());
}

#[test]
fn other_algorithms_are_refused() {
assert!(digest_to_sign(&SignatureAlgorithm::RsaPkcs1v15Sha384, b"x").is_err());
assert!(digest_to_sign(&SignatureAlgorithm::Ecdsa, &[0u8; 32]).is_err());
}
}
159 changes: 159 additions & 0 deletions tests/pre-sign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
#
# Live checks run against the Certum cloud before a release is signed: every
# way a user drives ssign must produce a valid signature, or nothing ships.
#
# 1. PKCS#11 mechanisms (pkcs11-tool): CKM_SHA256_RSA_PKCS, fed the message in
# several parts (C_SignUpdate/C_SignFinal), and CKM_RSA_PKCS, fed a SHA-256
# DigestInfo, give the same signature, and it verifies with the public key.
# Includes a 32-byte and a 51-byte message, which are still messages.
# 2. jsign through Java's SunPKCS11 (issue #11): PE + MSI, verified.
# 3. osslsigncode through the module: every format (sign-all-formats.sh).
# 4. The ssign CLI on a PE file, verified.
#
# The first sign logs in (CERTUM_OTP or CERTUM_TOKEN); the session cache makes
# every later step, in every process, reuse that login.
#
# Requirements: pkcs11-tool (opensc), osslsigncode + libengine-pkcs11-openssl,
# openssl, java 11+, and the jsign jar (JSIGN_JAR).
#
# Usage:
# CERTUM_EMAIL=you@example.com CERTUM_OTP=SEED JSIGN_JAR=jsign.jar \
# tests/pre-sign.sh target/release/ssign target/release/libssign_pkcs11.so

set -uo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIX="$ROOT/ssign-core/tests/fixtures"
INTER_DER="$ROOT/ssign-core/src/certs/ccsca2021.der"

SSIGN="$(realpath "${1:?usage: pre-sign.sh <ssign binary> <PKCS#11 module>}")"
MODULE="$(realpath "${2:?usage: pre-sign.sh <ssign binary> <PKCS#11 module>}")"
: "${CERTUM_EMAIL:?set CERTUM_EMAIL}"
[[ -n "${CERTUM_OTP:-}${CERTUM_TOKEN:-}" ]] || {
echo "error: set CERTUM_OTP (TOTP seed) or CERTUM_TOKEN (6-digit code)" >&2
exit 2
}
: "${JSIGN_JAR:?set JSIGN_JAR to the jsign jar}"

TSA=http://time.certum.pl/
LABEL="Certum SimplySign (ssign)"

mkdir -p "$ROOT/target"
WORK="$(mktemp -d "$ROOT/target/pre-sign.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT

failures=0
ok() { printf ' ✓ %s\n' "$1"; }
ko() { printf ' ✗ %s\n' "$1"; failures=$((failures + 1)); }

# Check the facts osslsigncode reports, not just its exit code: a digest
# mismatch or a missing timestamp still prints a lot of reassuring lines.
verify() {
local out
out=$(osslsigncode verify -in "$1" 2>&1) || true
if grep -q "MISMATCH" <<<"$out"; then
echo "$out"; echo " $1: Authenticode digest mismatch"; return 1
fi
if grep -q "Timestamp is not available" <<<"$out"; then
echo "$out"; echo " $1: no timestamp"; return 1
fi
if ! grep -q "Signature verification: ok" <<<"$out"; then
echo "$out"; return 1
fi
}

p11() { pkcs11-tool --module "$MODULE" "$@"; }

# --- 1. PKCS#11 mechanisms -------------------------------------------------
echo "== PKCS#11 mechanisms (pkcs11-tool)"

# The certificate from the module itself, and the Certum intermediate.
if p11 --read-object --type cert --label "$LABEL" -o "$WORK/leaf.der" >/dev/null 2>&1; then
openssl x509 -inform DER -in "$WORK/leaf.der" -out "$WORK/leaf.pem"
openssl x509 -in "$WORK/leaf.pem" -pubkey -noout >"$WORK/pub.pem"
ok "certificate read from the module"
else
ko "cannot read the certificate from the module"
p11 --read-object --type cert --label "$LABEL" -o "$WORK/leaf.der"
exit 1
fi
openssl x509 -inform DER -in "$INTER_DER" -out "$WORK/inter.pem"

# 1 MiB so pkcs11-tool streams it through several C_SignUpdate calls.
head -c 1048576 /dev/urandom >"$WORK/msg-large"
head -c 32 /dev/urandom >"$WORK/msg-32"
# 51 bytes shaped exactly like a SHA-256 DigestInfo.
{ printf '\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'
head -c 32 /dev/urandom; } >"$WORK/msg-51"

for m in msg-large msg-32 msg-51; do
msg="$WORK/$m"
if ! p11 --sign --mechanism SHA256-RSA-PKCS --label "$LABEL" \
-i "$msg" -o "$msg.sha256-rsa" >/dev/null 2>&1; then
ko "$m: CKM_SHA256_RSA_PKCS sign failed"
p11 --sign --mechanism SHA256-RSA-PKCS --label "$LABEL" -i "$msg" -o "$msg.sha256-rsa"
continue
fi
if openssl dgst -sha256 -verify "$WORK/pub.pem" -signature "$msg.sha256-rsa" "$msg" >/dev/null; then
ok "$m: CKM_SHA256_RSA_PKCS signature verifies"
else
ko "$m: CKM_SHA256_RSA_PKCS signature does not verify"
fi
{ printf '\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'
openssl dgst -sha256 -binary "$msg"; } >"$msg.digestinfo"
if p11 --sign --mechanism RSA-PKCS --label "$LABEL" \
-i "$msg.digestinfo" -o "$msg.rsa" >/dev/null 2>&1 \
&& cmp -s "$msg.sha256-rsa" "$msg.rsa"; then
ok "$m: CKM_RSA_PKCS over its DigestInfo gives the same signature"
else
ko "$m: CKM_RSA_PKCS over its DigestInfo differs or failed"
fi
done

# --- 2. jsign (Java SunPKCS11) ---------------------------------------------
echo "== jsign $(basename "$JSIGN_JAR") through SunPKCS11"
printf 'name = ssign\nlibrary = %s\n' "$MODULE" >"$WORK/pkcs11.cfg"
# The module only holds the leaf; jsign gets the full chain from a .p7b.
openssl crl2pkcs7 -nocrl -certfile "$WORK/leaf.pem" -certfile "$WORK/inter.pem" \
-outform DER -out "$WORK/chain.p7b"
cp "$FIX/hello.exe" "$WORK/jsign.exe"
cp "$FIX/test.msi" "$WORK/jsign.msi"
for f in jsign.exe jsign.msi; do
if java -jar "$JSIGN_JAR" --storetype PKCS11 --keystore "$WORK/pkcs11.cfg" \
--storepass "" --alias "$LABEL" --certfile "$WORK/chain.p7b" \
--alg SHA-256 --tsaurl "$TSA" "$WORK/$f" >"$WORK/$f.log" 2>&1 \
&& verify "$WORK/$f"; then
ok "$f signed by jsign and verified"
else
ko "$f: jsign failed"
cat "$WORK/$f.log"
fi
done

# --- 3. osslsigncode, every format -----------------------------------------
echo "== osslsigncode through the module"
if SSIGN_PKCS11_MODULE="$MODULE" "$ROOT/ssign-pkcs11/tests/sign-all-formats.sh"; then
ok "every format signed and verified"
else
ko "sign-all-formats.sh failed"
fi

# --- 4. ssign CLI ------------------------------------------------------------
echo "== ssign CLI"
cp "$FIX/hello.exe" "$WORK/cli.exe"
if "$SSIGN" -n "ssign pre-sign check" "$WORK/cli.exe" >"$WORK/cli.log" 2>&1 \
&& verify "$WORK/cli.exe"; then
ok "cli.exe signed by ssign and verified"
else
ko "ssign CLI failed"
cat "$WORK/cli.log"
fi

echo
if [[ $failures -eq 0 ]]; then
echo "pre-sign checks: all passed"
else
echo "pre-sign checks: $failures failed"
exit 1
fi