• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pgpainless / cert-d-java / #5

29 Sep 2025 11:40AM UTC coverage: 83.865% (-1.2%) from 85.102%
#5

push

other

vanitasvitae
Update changelog

421 of 502 relevant lines covered (83.86%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.47
/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java
1
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package pgp.cert_d;
6

7
import pgp.cert_d.subkey_lookup.SubkeyLookup;
8
import pgp.certificate_store.certificate.Certificate;
9
import pgp.certificate_store.certificate.KeyMaterial;
10
import pgp.certificate_store.certificate.KeyMaterialMerger;
11
import pgp.certificate_store.exception.BadDataException;
12
import pgp.certificate_store.exception.BadNameException;
13

14
import java.io.IOException;
15
import java.io.InputStream;
16
import java.util.Iterator;
17
import java.util.List;
18
import java.util.NoSuchElementException;
19
import java.util.Objects;
20
import java.util.Set;
21
import java.util.regex.Pattern;
22

23
/**
24
 * Implementation of the Shared PGP Certificate Directory.
25
 *
26
 * @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory Specification</a>
27
 */
28
public class PGPCertificateDirectory
29
        implements ReadOnlyPGPCertificateDirectory, WritingPGPCertificateDirectory, SubkeyLookup {
30

31
    final Backend backend;
32
    final SubkeyLookup subkeyLookup;
33
    private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
1✔
34
    private final Pattern openPgpV6FingerprintPattern = Pattern.compile("^[a-f0-9]{64}$");
1✔
35

36
    /**
37
     * Constructor for a PGP certificate directory.
38
     *
39
     * @param backend storage backend
40
     * @param subkeyLookup subkey lookup mechanism to map subkey-ids to certificates
41
     */
42
    public PGPCertificateDirectory(Backend backend, SubkeyLookup subkeyLookup) {
1✔
43
        this.backend = backend;
1✔
44
        this.subkeyLookup = subkeyLookup;
1✔
45
    }
1✔
46

47
    @Override
48
    public Certificate getByFingerprint(String fingerprint) throws BadDataException, BadNameException, IOException {
49
        if (!openPgpV4FingerprintPattern.matcher(fingerprint).matches() &&
1✔
50
                !openPgpV6FingerprintPattern.matcher(fingerprint).matches()) {
1✔
51
            throw new BadNameException();
1✔
52
        }
53
        Certificate certificate = backend.readByFingerprint(fingerprint);
1✔
54
        if (certificate == null) {
1✔
55
            throw new NoSuchElementException();
1✔
56
        }
57
        return certificate;
1✔
58
    }
59

60
    @Override
61
    public Certificate getByFingerprintIfChanged(String fingerprint, long tag)
62
            throws IOException, BadNameException, BadDataException {
63
        if (!Objects.equals(tag, backend.getTagForFingerprint(fingerprint))) {
1✔
64
            return getByFingerprint(fingerprint);
1✔
65
        }
66
        return null;
1✔
67
    }
68

69

70
    @Override
71
    public Certificate getBySpecialName(String specialName)
72
            throws BadNameException, BadDataException, IOException {
73
        KeyMaterial keyMaterial = backend.readBySpecialName(specialName);
1✔
74
        if (keyMaterial != null) {
1✔
75
            return keyMaterial.asCertificate();
1✔
76
        }
77
        throw new NoSuchElementException();
1✔
78
    }
79

80
    @Override
81
    public Certificate getBySpecialNameIfChanged(String specialName, long tag)
82
            throws IOException, BadNameException, BadDataException {
83
        if (!Objects.equals(tag, backend.getTagForSpecialName(specialName))) {
1✔
84
            return getBySpecialName(specialName);
1✔
85
        }
86
        return null;
1✔
87
    }
88

89
    @Override
90
    public Certificate getTrustRootCertificate()
91
            throws IOException, BadDataException {
92
        try {
93
            return getBySpecialName(SpecialNames.TRUST_ROOT);
1✔
94
        } catch (BadNameException e) {
×
95
            throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST");
×
96
        }
97
    }
98

99
    @Override
100
    public Certificate getTrustRootCertificateIfChanged(long tag) throws IOException, BadDataException {
101
        try {
102
            return getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag);
1✔
103
        } catch (BadNameException e) {
×
104
            throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST");
×
105
        }
106
    }
107

108
    @Override
109
    public Iterator<Certificate> items() {
110
        return backend.readItems();
1✔
111
    }
112

113
    @Override
114
    public Iterator<String> fingerprints() {
115
        Iterator<Certificate> certs = items();
1✔
116
        return new Iterator<String>() {
1✔
117
            @Override
118
            public boolean hasNext() {
119
                return certs.hasNext();
1✔
120
            }
121

122
            @Override
123
            public String next() {
124
                return certs.next().getFingerprint();
1✔
125
            }
126
        };
127
    }
128

129
    @Override
130
    public KeyMaterial getTrustRoot() throws IOException, BadDataException {
131
        try {
132
            KeyMaterial keyMaterial = backend.readBySpecialName(SpecialNames.TRUST_ROOT);
1✔
133
            if (keyMaterial == null) {
1✔
134
                throw new NoSuchElementException();
1✔
135
            }
136
            return keyMaterial;
1✔
137
        } catch (BadNameException e) {
×
138
            throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST");
×
139
        }
140
    }
141

142
    @Override
143
    public KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge)
144
            throws IOException, BadDataException, InterruptedException {
145
        backend.getLock().lockDirectory();
1✔
146
        KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
1✔
147
        subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
1✔
148
        backend.getLock().releaseDirectory();
1✔
149
        return inserted;
1✔
150
    }
151

152
    @Override
153
    public KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
154
            throws IOException, BadDataException {
155
        if (!backend.getLock().tryLockDirectory()) {
1✔
156
            return null;
1✔
157
        }
158
        KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
1✔
159
        subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
1✔
160
        backend.getLock().releaseDirectory();
1✔
161
        return inserted;
1✔
162
    }
163

164

165

166
    @Override
167
    public Certificate insert(InputStream data, KeyMaterialMerger merge)
168
            throws IOException, BadDataException, InterruptedException {
169
        backend.getLock().lockDirectory();
1✔
170
        Certificate inserted = backend.doInsert(data, merge);
1✔
171
        subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
1✔
172
        backend.getLock().releaseDirectory();
1✔
173
        return inserted;
1✔
174
    }
175

176
    @Override
177
    public Certificate tryInsert(InputStream data, KeyMaterialMerger merge)
178
            throws IOException, BadDataException {
179
        if (!backend.getLock().tryLockDirectory()) {
1✔
180
            return null;
1✔
181
        }
182
        Certificate inserted = backend.doInsert(data, merge);
1✔
183
        subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
1✔
184
        backend.getLock().releaseDirectory();
1✔
185
        return inserted;
1✔
186
    }
187

188
    @Override
189
    public Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
190
            throws IOException, BadDataException, BadNameException, InterruptedException {
191
        backend.getLock().lockDirectory();
1✔
192
        Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge);
1✔
193
        subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
1✔
194
        backend.getLock().releaseDirectory();
1✔
195
        return inserted;
1✔
196
    }
197

198
    @Override
199
    public Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
200
            throws IOException, BadDataException, BadNameException {
201
        if (!backend.getLock().tryLockDirectory()) {
1✔
202
            return null;
1✔
203
        }
204
        Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge);
1✔
205
        subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
1✔
206
        backend.getLock().releaseDirectory();
1✔
207
        return inserted;
1✔
208
    }
209

210
    @Override
211
    public Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException {
212
        return subkeyLookup.getCertificateFingerprintsForSubkeyId(subkeyId);
1✔
213
    }
214

215
    @Override
216
    public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException {
217
        subkeyLookup.storeCertificateSubkeyIds(certificate, subkeyIds);
×
218
    }
×
219

220
    /**
221
     * Storage backend.
222
     */
223
    public interface Backend {
224

225
        /**
226
         * Get the locking mechanism to write-lock the backend.
227
         *
228
         * @return lock
229
         */
230
        LockingMechanism getLock();
231

232
        /**
233
         * Read a {@link Certificate} by its OpenPGP fingerprint.
234
         *
235
         * @param fingerprint fingerprint
236
         * @return certificate
237
         *
238
         * @throws BadNameException if the fingerprint is malformed
239
         * @throws IOException in case of an IO error
240
         * @throws BadDataException if the certificate contains bad data
241
         */
242
        Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException;
243

244
        /**
245
         * Read a {@link Certificate} or {@link pgp.certificate_store.certificate.Key} by the given special name.
246
         *
247
         * @param specialName special name
248
         * @return certificate or key
249
         *
250
         * @throws BadNameException if the special name is not known
251
         * @throws IOException in case of an IO error
252
         * @throws BadDataException if the certificate contains bad data
253
         */
254
        KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException;
255

256
        /**
257
         * Return an {@link Iterator} of all {@link Certificate Certificates} in the store, except for certificates
258
         * stored under a special name.
259
         *
260
         * @return iterator
261
         */
262
        Iterator<Certificate> readItems();
263

264
        /**
265
         * Insert a {@link pgp.certificate_store.certificate.Key} or {@link Certificate} as trust-root.
266
         *
267
         * @param data input stream containing the key material
268
         * @param merge callback to merge the key material with existing key material
269
         * @return merged or inserted key material
270
         *
271
         * @throws BadDataException if the data stream or existing key material contains bad data
272
         * @throws IOException in case of an IO error
273
         */
274
        KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
275
                throws BadDataException, IOException;
276

277
        /**
278
         * Insert a {@link Certificate} identified by its fingerprint into the directory.
279
         *
280
         * @param data input stream containing the certificate data
281
         * @param merge callback to merge the certificate with existing key material
282
         * @return merged or inserted certificate
283
         *
284
         * @throws IOException in case of an IO error
285
         * @throws BadDataException if the data stream or existing certificate contains bad data
286
         */
287
        Certificate doInsert(InputStream data, KeyMaterialMerger merge)
288
                        throws IOException, BadDataException;
289

290
        /**
291
         * Insert a {@link pgp.certificate_store.certificate.Key} or {@link Certificate} under the given special name.
292
         *
293
         * @param specialName special name to identify the key material with
294
         * @param data data stream containing the key or certificate
295
         * @param merge callback to merge the key/certificate with existing key material
296
         * @return certificate component of the merged or inserted key material
297
         *
298
         * @throws IOException in case of an IO error
299
         * @throws BadDataException if the data stream or existing key material contains bad data
300
         * @throws BadNameException if the special name is not known
301
         */
302
        Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
303
                                throws IOException, BadDataException, BadNameException;
304

305
        /**
306
         * Calculate the tag of the certificate with the given fingerprint.
307
         *
308
         * @param fingerprint fingerprint
309
         * @return tag
310
         *
311
         * @throws BadNameException if the fingerprint is malformed
312
         * @throws IOException in case of an IO error
313
         * @throws IllegalArgumentException if the certificate does not exist
314
         */
315
        Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException;
316

317
        /**
318
         * Calculate the tag of the certificate identified by the given special name.
319
         *
320
         * @param specialName special name
321
         * @return tag
322
         *
323
         * @throws BadNameException if the special name is not known
324
         * @throws IOException in case of an IO error
325
         * @throws IllegalArgumentException if the certificate or key does not exist
326
         */
327
        Long getTagForSpecialName(String specialName) throws BadNameException, IOException;
328
    }
329

330
    /**
331
     * Interface for a write-locking mechanism.
332
     */
333
    public interface LockingMechanism {
334

335
        /**
336
         * Lock the store for writes.
337
         * Readers can continue to use the store and will always see consistent certs.
338
         *
339
         * @throws IOException in case of an IO error
340
         * @throws InterruptedException if the thread gets interrupted
341
         */
342
        void lockDirectory() throws IOException, InterruptedException;
343

344
        /**
345
         * Try top lock the store for writes.
346
         * Return false without locking the store in case the store was already locked.
347
         *
348
         * @return true if locking succeeded, false otherwise
349
         *
350
         * @throws IOException in case of an IO error
351
         */
352
        boolean tryLockDirectory() throws IOException;
353

354
        /**
355
         * Return true if the lock is in locked state.
356
         *
357
         * @return true if locked
358
         */
359
        boolean isLocked();
360

361
        /**
362
         * Release the directory write-lock acquired via {@link #lockDirectory()}.
363
         *
364
         * @throws IOException in case of an IO error
365
         */
366
        void releaseDirectory() throws IOException;
367

368
    }
369
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc