Extract keys, fingerprints and identities from GPG keys.
This artifact runs the tool “gpg” (must be installed on the system) on the files found matching the globs in KeyringFiles. The files need not be keyrings.
Every entry consists of a public or secret key, optional subkeys and optional identities. This artifact may be useful in other artifacts to inspect GPG files or GPG data in order to correlate keys by their IDs, or look at connected user IDs.
This artifact doesn’t provide any information about whether a key is “trustworthy”.
Note that some keyring files contain a lot of subkeys and identities.
The following columns are returned by this artifact:
name: Linux.Debian.GPGKeys
description: |
Extract keys, fingerprints and identities from GPG keys.
This artifact runs the tool "gpg" (must be installed on the system) on the
files found matching the globs in KeyringFiles. The files need not be keyrings.
Every entry consists of a public or secret key, optional subkeys and optional
identities. This artifact may be useful in other artifacts to inspect GPG
files or GPG data in order to correlate keys by their IDs, or look at connected
user IDs.
This artifact doesn't provide any information about whether a key is
"trustworthy".
Note that some keyring files contain a lot of subkeys and identities.
The following columns are returned by this artifact:
- OSPath: Path to the key file
- KeyInfo: dict with the following entries:
- Type: pub|sub
- ID
- Fingerprint
- Algorithm
- Validity
- Created
- Expiry
- SubKeys: array of dicts with the same structure as KeyInfo
- UserIDs: array of strings (name and e-mail)
reference:
- https://manpages.debian.org/bookworm/apt/apt-key.8.en.html
- https://github.com/CSNW/gnupg/blob/master/doc/DETAILS
- https://www.mailpile.is/blog/2014-10-07_Some_Thoughts_on_GnuPG.html
- https://www.ietf.org/rfc/rfc4880.txt
export: |
/* Extract machine-"readable" data from the GPG keys found in the file.
The format is documented in the reference above. However, as the blog
post mentions, detailed knowledge about GPG is needed in order to
decipher the output. See ParseKeyInfo_(). */
LET InspectGPGFile(filename) = SELECT Stdout AS Info
FROM execve(argv=['gpg', '--with-colons', filename])
/* Pipe data to the same command as in InspectGPGFile(): */
LET InspectGPGData(data) = SELECT *
FROM InspectGPGFile(filename=tempfile(data=data))
/* Convert the validity code to a more human-readable string (see
reference for details): */
LET GPGValidityString(validity) = regex_transform(source=validity, map=dict(
`^o$`='Unknown',
`^i$`='Invalid',
`^d$`='Disabled',
`^r$`='Revoked',
`^e$`='Expired',
`^-$`='Unknown',
`^q$`='Unknown',
`^n$`='Invalid',
`^m$`='Marginally valid',
`^f$`='Fully valid',
`^u$`='Ultimately valid',
`^w$`='Well-known',
`^s$`='Special'
))
/* Convert timestamp, but only if it is non-null: */
LET MaybeTimestamp(epoch) = if(
condition=epoch, then=timestamp(epoch=epoch), else=null)
LET ParseKeyInfo_(data) = SELECT * FROM foreach(
/* A file may contain several "keys" (i.e. sections of either a
public or private key, followed by a fingerprint, subkeys and
identities). In order to parse these sections, the contents of
the file are split (an arbitraray binary blob is used): */
row={SELECT split(sep_string='\x01\x02\0x03',
string=regex_replace(source=data, re='(?m)^(pub|sec):',replace='\x01\x02\x03$1')) AS KeyInfo
FROM scope()},
query={
/* There is only one key (public or private) followed by an
optional fingerprint: */
SELECT parse_string_with_regex(string=KeyInfo, regex=(
'''(?m)(?P<Type>pub|sec):(?P<Validity>[^:]*):(?P<Length>[^:]*):(?P<Algorithm>[^:]*):(?P<ID>[^:]*):(?P<Created>[^:]*):(?P<Expiry>[^:]*):[^:]*:(?P<Trust>[^:]*)''',
'''fpr:::::::::(?P<Fingerprint>[^:]*)'''
)) AS KeyInfo,
/* There may be none or several subkeys (following the same
format as public/private keys): */
{SELECT Type,
ID,
Fingerprint,
atoi(string=Algorithm) AS Algorithm,
GPGValidityString(validity=Validity) AS Validity,
MaybeTimestamp(epoch=Created) AS Created,
MaybeTimestamp(epoch=Expiry) AS Expiry
FROM parse_records_with_regex(
file=KeyInfo,
accessor='data',
regex=(
'''(?m)(?P<Type>sub):(?P<Validity>[^:]*):(?P<Length>[^:]*):(?P<Algorithm>[^:]*):(?P<ID>[^:]*):(?P<Created>[^:]*):(?P<Expiry>[^:]*):[^:]*:(?P<Trust>[^:]*)''',
'''fpr:::::::::(?P<Fingerprint>[^:]*)'''
))
} AS SubKeys,
/* There may be none or several identities: */
array(uids={SELECT UserID FROM parse_records_with_regex(
file=KeyInfo,
accessor='data',
regex='''uid:::::::::(?P<UserID>[^:]*)''')}) AS UserIDs
FROM scope()
WHERE KeyInfo
})
LET ParseKeyInfo(data) = SELECT dict(
Type=KeyInfo.Type,
ID=KeyInfo.ID,
Fingerprint=get(item=KeyInfo, field='Fingerprint', default=''),
Algorithm=atoi(string=KeyInfo.Algorithm),
Validity=GPGValidityString(validity=KeyInfo.Validity),
Created=MaybeTimestamp(epoch=KeyInfo.Created),
Expiry=MaybeTimestamp(epoch=KeyInfo.Expiry)
) AS KeyInfo,
SubKeys,
UserIDs
FROM ParseKeyInfo_(data=data)
LET ParseGPG(data) = SELECT *
FROM ParseKeyInfo(data=InspectGPGData(data=data))
LET ParseGPGFile(filename) = SELECT *
FROM ParseKeyInfo(data=InspectGPGFile(filename=filename))
parameters:
- name: KeyringFiles
description: Globs to find GPG keyrings
type: csv
default: |
KeyringGlobs
/etc/apt/trusted.gpg
/etc/apt/trusted.gpg.d/*.gpg
/etc/apt/keyrings/*.gpg
/usr/share/keyrings/*.gpg
precondition:
SELECT OS From info() where OS = 'linux'
sources:
- name: KeyringKeys
query: |
LET GPGKeys = SELECT * FROM foreach(
row={SELECT OSPath FROM glob(globs=KeyringFiles.KeyringGlobs)},
query={SELECT OSPath, * FROM ParseGPGFile(filename=OSPath)}
)
SELECT * FROM GPGKeys