001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org> 002// 003// SPDX-License-Identifier: Apache-2.0 004 005package org.pgpainless.key.util; 006 007import javax.annotation.Nonnull; 008import java.util.Map; 009import java.util.concurrent.ConcurrentHashMap; 010 011public final class RevocationAttributes { 012 013 /** 014 * Reason for revocation. 015 * There are two kinds of reasons: hard and soft reason. 016 * 017 * Soft revocation reasons gracefully disable keys or user-ids. 018 * Softly revoked keys can no longer be used to encrypt data to or to generate signatures. 019 * Any signature made after a key has been soft revoked is deemed invalid. 020 * Any signature made before the key has been soft revoked stays valid. 021 * Soft revoked info can be re-certified at a later point. 022 * 023 * Hard revocation reasons on the other hand renders the key or user-id invalid immediately. 024 * Hard reasons are suitable to use if for example a key got compromised. 025 * Any signature made before or after a key has been hard revoked is no longer considered valid. 026 * Hard revoked information can also not be re-certified. 027 */ 028 public enum Reason { 029 /** 030 * The key or certification is being revoked without a reason. 031 * This is a HARD revocation reason and cannot be undone. 032 */ 033 NO_REASON((byte) 0), 034 /** 035 * The key was superseded by another key. 036 * This is a SOFT revocation reason and can be undone. 037 */ 038 KEY_SUPERSEDED((byte) 1), 039 /** 040 * The key has potentially been compromised. 041 * This is a HARD revocation reason and cannot be undone. 042 */ 043 KEY_COMPROMISED((byte) 2), 044 /** 045 * The key was retired and shall no longer be used. 046 * This is a SOFT revocation reason can can be undone. 047 */ 048 KEY_RETIRED((byte) 3), 049 /** 050 * The user-id is no longer valid. 051 * This is a SOFT revocation reason and can be undone. 052 */ 053 USER_ID_NO_LONGER_VALID((byte) 32), 054 ; 055 056 private static final Map<Byte, Reason> MAP = new ConcurrentHashMap<>(); 057 static { 058 for (Reason r : Reason.values()) { 059 MAP.put(r.reasonCode, r); 060 } 061 } 062 063 /** 064 * Decode a machine-readable reason code. 065 * 066 * @param code byte 067 * @return reason 068 */ 069 public static Reason fromCode(byte code) { 070 Reason reason = MAP.get(code); 071 if (reason == null) { 072 throw new IllegalArgumentException("Invalid revocation reason: " + code); 073 } 074 return reason; 075 } 076 077 /** 078 * Return true if the {@link Reason} the provided code encodes is a hard revocation reason, false 079 * otherwise. 080 * Hard revocations cannot be undone, while keys or certifications with soft revocations can be 081 * re-certified by placing another signature on them. 082 * 083 * @param code reason code 084 * @return is hard 085 */ 086 public static boolean isHardRevocation(byte code) { 087 Reason reason = MAP.get(code); 088 return reason != KEY_SUPERSEDED && reason != KEY_RETIRED && reason != USER_ID_NO_LONGER_VALID; 089 } 090 091 /** 092 * Return true if the given {@link Reason} is a hard revocation, false otherwise. 093 * Hard revocations cannot be undone, while keys or certifications with soft revocations can be 094 * re-certified by placing another signature on them. 095 * 096 * @param reason reason 097 * @return is hard 098 */ 099 public static boolean isHardRevocation(@Nonnull Reason reason) { 100 return isHardRevocation(reason.reasonCode); 101 } 102 103 private final byte reasonCode; 104 105 Reason(byte reasonCode) { 106 this.reasonCode = reasonCode; 107 } 108 109 public byte code() { 110 return reasonCode; 111 } 112 113 @Override 114 public String toString() { 115 return code() + " - " + name(); 116 } 117 } 118 119 public enum RevocationType { 120 KEY_REVOCATION, 121 CERT_REVOCATION 122 } 123 124 private final Reason reason; 125 private final String description; 126 127 private RevocationAttributes(Reason reason, String description) { 128 this.reason = reason; 129 this.description = description; 130 } 131 132 /** 133 * Return the machine-readable reason for revocation. 134 * 135 * @return reason 136 */ 137 public @Nonnull Reason getReason() { 138 return reason; 139 } 140 141 /** 142 * Return the human-readable description for the revocation reason. 143 * @return description 144 */ 145 public @Nonnull String getDescription() { 146 return description; 147 } 148 149 /** 150 * Build a {@link RevocationAttributes} object suitable for key revocations. 151 * Key revocations are revocations for keys or subkeys. 152 * 153 * @return builder 154 */ 155 public static WithReason createKeyRevocation() { 156 return new WithReason(RevocationType.KEY_REVOCATION); 157 } 158 159 /** 160 * Build a {@link RevocationAttributes} object suitable for certification (e.g. user-id) revocations. 161 * 162 * @return builder 163 */ 164 public static WithReason createCertificateRevocation() { 165 return new WithReason(RevocationType.CERT_REVOCATION); 166 } 167 168 public static final class WithReason { 169 170 private final RevocationType type; 171 172 private WithReason(RevocationType type) { 173 this.type = type; 174 } 175 176 /** 177 * Set the machine-readable reason. 178 * Note that depending on whether this is a key-revocation or certification-revocation, 179 * only certain reason codes are valid. 180 * Invalid input will result in an {@link IllegalArgumentException} to be thrown. 181 * 182 * @param reason reason 183 * @throws IllegalArgumentException in case of an invalid revocation reason 184 * @return builder 185 */ 186 public WithDescription withReason(Reason reason) { 187 throwIfReasonTypeMismatch(reason, type); 188 return new WithDescription(reason); 189 } 190 191 private void throwIfReasonTypeMismatch(Reason reason, RevocationType type) { 192 if (type == RevocationType.KEY_REVOCATION) { 193 if (reason == Reason.USER_ID_NO_LONGER_VALID) { 194 throw new IllegalArgumentException("Reason " + reason + " can only be used for certificate revocations, not to revoke keys."); 195 } 196 } else if (type == RevocationType.CERT_REVOCATION) { 197 switch (reason) { 198 case KEY_SUPERSEDED: 199 case KEY_COMPROMISED: 200 case KEY_RETIRED: 201 throw new IllegalArgumentException("Reason " + reason + " can only be used for key revocations, not to revoke certificates."); 202 } 203 } 204 } 205 206 } 207 208 public static final class WithDescription { 209 210 private final Reason reason; 211 212 private WithDescription(Reason reason) { 213 this.reason = reason; 214 } 215 216 /** 217 * Set a human-readable description of the revocation reason. 218 * 219 * @param description description 220 * @return revocation attributes 221 */ 222 public RevocationAttributes withDescription(@Nonnull String description) { 223 return new RevocationAttributes(reason, description); 224 } 225 226 /** 227 * Set an empty human-readable description. 228 * @return revocation attributes 229 */ 230 public RevocationAttributes withoutDescription() { 231 return withDescription(""); 232 } 233 } 234}