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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* text eol=lf
* text=auto eol=lf
7 changes: 5 additions & 2 deletions NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
v0.1.31

New:
- Added `Sig.issuer_fingerprint`, `Sig.issuer_key_id`, `Sig.signers_user_id`, and `Sig.expiration` [#60]
- Added docstrings to the `Sig` API [#60]

Changed:
- This release changes metadata of the project and the release workflow. There are no functional changes. See changelog for version 0.1.30.
- Changed Rust edition from 2021 to 2024.
- `Sig.issuer_fpr` is now considered deprecated, use `Sig.issuer_fingerprint` instead [#60]

Removed:

[#60]: https://github.com/wiktor-k/pysequoia/pull/60

### Release checklist:
### [ ] Change version in `Cargo.toml` and `pyproject.toml` and `NEXT.md`
### [ ] Update dependencies via `cargo update`
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,11 @@ sig = Sig.from_file("sig.pgp")

print(f"Parsed signature: {repr(sig)}")

assert sig.issuer_fpr == "e8f23996f23218640cb44cbe75cf5ac418b8e74c"
assert sig.issuer_fingerprint == "e8f23996f23218640cb44cbe75cf5ac418b8e74c"
assert sig.issuer_key_id == "75cf5ac418b8e74c"
assert sig.created == datetime.fromisoformat("2023-07-19T18:14:01+00:00")
assert sig.expiration == None
assert sig.signers_user_id == None
```

## License
Expand Down
8 changes: 8 additions & 0 deletions pysequoia.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,20 @@ class Sig:
def __str__(self, /) -> str: ...
@property
def created(self, /) -> datetime |None: ...
@property
def expiration(self, /) -> datetime |None: ...
@staticmethod
def from_bytes(bytes: bytes) -> Sig: ...
@staticmethod
def from_file(path: str) -> Sig: ...
@property
def issuer_fingerprint(self, /) -> str |None: ...
@property
def issuer_fpr(self, /) -> str |None: ...
@property
def issuer_key_id(self, /) -> str |None: ...
@property
def signers_user_id(self, /) -> str |None: ...

@final
class SignatureMode:
Expand Down
68 changes: 67 additions & 1 deletion src/signature.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(deprecated)]

use std::borrow::Cow;

use anyhow::anyhow;
Expand All @@ -14,10 +16,14 @@ pub struct Sig {
}

impl Sig {
/// Wraps a raw Sequoia [`SqSignature`] packet.
pub fn new(sig: SqSignature) -> Self {
Self { sig }
}

/// Extracts the first signature packet from a [`PacketParserResult`].
///
/// Returns an error if the parser result is empty or the first packet is not a signature.
pub fn from_packets(ppr: PacketParserResult<'_>) -> Result<Self, anyhow::Error> {
if let PacketParserResult::Some(pp) = ppr {
let (packet, _next_ppr) = pp.recurse()?;
Expand All @@ -37,40 +43,100 @@ impl From<SqSignature> for Sig {

#[pymethods]
impl Sig {
/// Loads a signature from a file on disk.
///
/// The file may be binary or ASCII-armored.
#[staticmethod]
pub fn from_file(path: String) -> PyResult<Self> {
Ok(Self::from_packets(PacketParser::from_file(path)?)?)
}

/// Loads a signature from a byte string.
///
/// The bytes may be binary or ASCII-armored.
#[staticmethod]
pub fn from_bytes(bytes: &[u8]) -> PyResult<Self> {
Ok(Self::from_packets(PacketParser::from_bytes(bytes)?)?)
}

/// Returns the raw binary encoding of the signature packet.
pub fn __bytes__(&self) -> PyResult<Cow<'_, [u8]>> {
Ok(crate::serialize(self.sig.clone().into(), None)?.into())
}

/// DEPRECATED: The fingerprint of the key that made this signature, as a lowercase hex string.
///
/// Alias for `issuer_fingerprint`. Prefer `issuer_fingerprint` going forwards.
///
/// Returns `None` if the signature does not carry an issuer fingerprint subpacket.
/// Prefer this over `issuer_key_id` when available, as fingerprints are collision-resistant.
#[getter]
#[deprecated = "Prefer Sig.issuer_fingerprint"]
pub fn issuer_fpr(&self) -> Option<String> {
self.issuer_fingerprint()
}

/// The fingerprint of the key that made this signature, as a lowercase hex string.
///
/// Returns `None` if the signature does not carry an issuer fingerprint subpacket.
#[getter]
pub fn issuer_fingerprint(&self) -> Option<String> {
self.sig
.issuer_fingerprints()
.next()
.map(|issuer| format!("{issuer:x}"))
}

/// The short key ID of the key that made this signature, as a lowercase hex string.
///
/// Returns `None` if the signature does not carry an issuer key ID subpacket.
/// Prefer `issuer_fingerprint` over this where possible, as key IDs are not collision-resistant.
#[getter]
pub fn issuer_key_id(&self) -> Option<String> {
self.sig.issuers().next().map(|id| format!("{id:x}"))
}

/// The User ID of the signer, as declared in the signature's Signer's User ID subpacket.
///
/// Returns `None` if the signature does not carry a Signer's User ID subpacket.
/// Note that this value is self-reported by the signer and is not verified against any cert.
#[getter]
pub fn signers_user_id(&self) -> Option<String> {
self.sig
.signers_user_id()
.map(|uid| String::from_utf8_lossy(uid).into_owned())
}

/// The time at which this signature was created.
///
/// Returns `None` if the signature does not carry a creation time subpacket.
#[getter]
pub fn created(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.sig.signature_creation_time().map(Into::into)
}

/// The time at which this signature expires, or `None` if it does not expire.
///
/// Computed as the signature creation time plus the signature validity period.
/// Returns `None` if either subpacket is absent.
#[getter]
pub fn expiration(&self) -> Option<chrono::DateTime<chrono::Utc>> {
let created = self.sig.signature_creation_time()?;
let validity = self.sig.signature_validity_period()?;
Some(created.checked_add(validity)?.into())
}

/// Return the ASCII-armored representation of the signature.
pub fn __str__(&self) -> PyResult<String> {
let bytes = crate::serialize(self.sig.clone().into(), armor::Kind::Signature)?;
Ok(String::from_utf8(bytes)?)
}

pub fn __repr__(&self) -> String {
format!("<Sig issuer_fpr={}>", self.issuer_fpr().unwrap_or_default())
format!(
"<Sig issuer_fingerprint={}>",
self.issuer_fingerprint().unwrap_or_default()
)
}
}

Expand Down