001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org> 002// 003// SPDX-License-Identifier: Apache-2.0 004 005package org.pgpainless.signature.consumer; 006 007import java.util.ArrayList; 008import java.util.Collections; 009import java.util.List; 010import java.util.NoSuchElementException; 011import java.util.regex.Matcher; 012import java.util.regex.Pattern; 013import javax.annotation.Nullable; 014 015import org.bouncycastle.bcpg.sig.NotationData; 016import org.bouncycastle.openpgp.PGPException; 017import org.bouncycastle.openpgp.PGPPublicKey; 018import org.bouncycastle.openpgp.PGPSecretKey; 019import org.bouncycastle.openpgp.PGPSecretKeyRing; 020import org.bouncycastle.openpgp.PGPSignature; 021import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; 022import org.pgpainless.PGPainless; 023import org.pgpainless.key.info.KeyRingInfo; 024import org.pgpainless.key.protection.SecretKeyRingProtector; 025import org.pgpainless.signature.builder.DirectKeySignatureBuilder; 026import org.pgpainless.signature.builder.SelfSignatureBuilder; 027 028public class ProofUtil { 029 030 public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, Proof proof) 031 throws PGPException { 032 return addProofs(secretKey, protector, Collections.singletonList(proof)); 033 } 034 035 public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, List<Proof> proofs) 036 throws PGPException { 037 KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); 038 return addProofs(secretKey, protector, info.getPrimaryUserId(), proofs); 039 } 040 041 public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, String userId, Proof proof) 042 throws PGPException { 043 return addProofs(secretKey, protector, userId, Collections.singletonList(proof)); 044 } 045 046 public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, 047 @Nullable String userId, List<Proof> proofs) 048 throws PGPException { 049 KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); 050 PGPSecretKey certificationKey = secretKey.getSecretKey(); 051 PGPPublicKey certificationPubKey = certificationKey.getPublicKey(); 052 PGPSignature certification = null; 053 054 // null userid -> make direct key sig 055 if (userId == null) { 056 PGPSignature previousCertification = info.getLatestDirectKeySelfSignature(); 057 if (previousCertification == null) { 058 throw new NoSuchElementException("No previous valid direct key signature found."); 059 } 060 061 DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(certificationKey, protector, previousCertification); 062 for (Proof proof : proofs) { 063 sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue()); 064 } 065 certification = sigBuilder.build(certificationPubKey); 066 certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, certification); 067 } else { 068 if (!info.isUserIdValid(userId)) { 069 throw new IllegalArgumentException("User ID " + userId + " seems to not be valid for this key."); 070 } 071 PGPSignature previousCertification = info.getLatestUserIdCertification(userId); 072 if (previousCertification == null) { 073 throw new NoSuchElementException("No previous valid user-id certification found."); 074 } 075 076 SelfSignatureBuilder sigBuilder = new SelfSignatureBuilder(certificationKey, protector, previousCertification); 077 for (Proof proof : proofs) { 078 sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue()); 079 } 080 certification = sigBuilder.build(certificationPubKey, userId); 081 certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, userId, certification); 082 } 083 certificationKey = PGPSecretKey.replacePublicKey(certificationKey, certificationPubKey); 084 secretKey = PGPSecretKeyRing.insertSecretKey(secretKey, certificationKey); 085 086 return secretKey; 087 } 088 089 public static class Proof { 090 public static final String NOTATION_NAME = "proof@metacode.biz"; 091 private final String notationValue; 092 093 public Proof(String notationValue) { 094 if (notationValue == null) { 095 throw new IllegalArgumentException("Notation value cannot be null."); 096 } 097 String trimmed = notationValue.trim(); 098 if (trimmed.isEmpty()) { 099 throw new IllegalArgumentException("Notation value cannot be empty."); 100 } 101 this.notationValue = trimmed; 102 } 103 104 public String getNotationName() { 105 return NOTATION_NAME; 106 } 107 108 public String getNotationValue() { 109 return notationValue; 110 } 111 112 public static Proof fromMatrixPermalink(String username, String eventPermalink) { 113 Pattern pattern = Pattern.compile("^https:\\/\\/matrix\\.to\\/#\\/(![a-zA-Z]{18}:matrix\\.org)\\/(\\$[a-zA-Z0-9\\-_]{43})\\?via=.*$"); 114 Matcher matcher = pattern.matcher(eventPermalink); 115 if (!matcher.matches()) { 116 throw new IllegalArgumentException("Invalid matrix event permalink."); 117 } 118 String roomId = matcher.group(1); 119 String eventId = matcher.group(2); 120 return new Proof(String.format("matrix:u/%s?org.keyoxide.r=%s&org.keyoxide.e=%s", username, roomId, eventId)); 121 } 122 123 @Override 124 public String toString() { 125 return getNotationName() + "=" + getNotationValue(); 126 } 127 } 128 129 public static List<Proof> getProofs(PGPSignature signature) { 130 PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); 131 NotationData[] notations = hashedSubpackets.getNotationDataOccurrences(); 132 133 List<Proof> proofs = new ArrayList<>(); 134 for (NotationData notation : notations) { 135 if (notation.getNotationName().equals(Proof.NOTATION_NAME)) { 136 proofs.add(new Proof(notation.getNotationValue())); 137 } 138 } 139 return proofs; 140 } 141}