Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/dsse.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

67 statements  

1"""Dead Simple Signing Envelope""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from typing import Any 

7 

8from securesystemslib import exceptions 

9from securesystemslib._internal.utils import b64dec, b64enc, make_hashable 

10from securesystemslib.signer import Key, Signature, Signer 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15class Envelope: 

16 """DSSE Envelope to provide interface for signing arbitrary data. 

17 

18 Attributes: 

19 payload: Arbitrary byte sequence of serialized body. 

20 payload_type: string that identifies how to interpret payload. 

21 signatures: dict of Signature key id and Signatures. 

22 

23 """ 

24 

25 def __init__( 

26 self, 

27 payload: bytes, 

28 payload_type: str, 

29 signatures: dict[str, Signature], 

30 ): 

31 self.payload = payload 

32 self.payload_type = payload_type 

33 self.signatures = signatures 

34 

35 def __eq__(self, other: Any) -> bool: 

36 if not isinstance(other, Envelope): 

37 return False 

38 

39 return ( 

40 self.payload == other.payload 

41 and self.payload_type == other.payload_type 

42 and self.signatures == other.signatures 

43 ) 

44 

45 def __hash__(self) -> int: 

46 return hash( 

47 ( 

48 self.payload, 

49 self.payload_type, 

50 make_hashable(self.signatures), 

51 ) 

52 ) 

53 

54 @classmethod 

55 def from_dict(cls, data: dict) -> Envelope: 

56 """Creates a DSSE Envelope from its JSON/dict representation. 

57 

58 Arguments: 

59 data: A dict containing a valid payload, payloadType and signatures 

60 

61 Raises: 

62 KeyError: If any of the "payload", "payloadType" and "signatures" 

63 fields are missing from the "data". 

64 

65 FormatError: If signature in "signatures" is incorrect. 

66 

67 Returns: 

68 A "Envelope" instance. 

69 """ 

70 

71 payload = b64dec(data["payload"]) 

72 payload_type = data["payloadType"] 

73 

74 signatures = {} 

75 for signature in data["signatures"]: 

76 signature["sig"] = b64dec(signature["sig"]).hex() 

77 sig = Signature.from_dict(signature) 

78 if sig.keyid in signatures: 

79 raise ValueError(f"Multiple signatures found for keyid {sig.keyid}") 

80 signatures[sig.keyid] = sig 

81 return cls(payload, payload_type, signatures) 

82 

83 def to_dict(self) -> dict: 

84 """Returns the JSON-serializable dictionary representation of self.""" 

85 

86 signatures = [] 

87 for signature in self.signatures.values(): 

88 sig_dict = signature.to_dict() 

89 sig_dict["sig"] = b64enc(bytes.fromhex(sig_dict["sig"])) 

90 signatures.append(sig_dict) 

91 

92 return { 

93 "payload": b64enc(self.payload), 

94 "payloadType": self.payload_type, 

95 "signatures": signatures, 

96 } 

97 

98 def pae(self) -> bytes: 

99 """Pre-Auth-Encoding byte sequence of self.""" 

100 

101 payload_type = self.payload_type.encode("utf-8") 

102 return b"DSSEv1 %d %b %d %b" % ( 

103 len(payload_type), 

104 payload_type, 

105 len(self.payload), 

106 self.payload, 

107 ) 

108 

109 def sign(self, signer: Signer) -> Signature: 

110 """Sign the payload and create the signature. 

111 

112 Arguments: 

113 signer: A "Signer" class instance. 

114 

115 Returns: 

116 A "Signature" instance. 

117 """ 

118 

119 signature = signer.sign(self.pae()) 

120 self.signatures[signature.keyid] = signature 

121 

122 return signature 

123 

124 def verify(self, keys: list[Key], threshold: int) -> dict[str, Key]: 

125 """Verify the payload with the provided Keys. 

126 

127 Arguments: 

128 keys: A list of public keys to verify the signatures. 

129 threshold: Number of signatures needed to pass the verification. 

130 

131 Raises: 

132 ValueError: If "threshold" is not valid. 

133 VerificationError: If the enclosed signatures do not pass the 

134 verification. 

135 

136 Note: 

137 Mandating keyid in signatures and matching them with keyid of Key 

138 in order to consider them for verification, is not DSSE spec 

139 compliant (Issue #416). 

140 

141 Returns: 

142 A dict of the threshold of unique public keys that verified a 

143 signature. 

144 """ 

145 

146 accepted_keys = {} 

147 pae = self.pae() 

148 

149 # checks for threshold value. 

150 if threshold <= 0: 

151 raise ValueError("Threshold must be greater than 0") 

152 

153 if len(keys) < threshold: 

154 raise ValueError("Number of keys can't be less than threshold") 

155 

156 for signature in self.signatures.values(): 

157 for key in keys: 

158 # If Signature keyid doesn't match with Key, skip. 

159 if not key.keyid == signature.keyid: 

160 continue 

161 

162 # If a key verifies the signature, we exit and use the result. 

163 try: 

164 key.verify_signature(signature, pae) 

165 accepted_keys[key.keyid] = key 

166 break 

167 except exceptions.UnverifiedSignatureError: 

168 continue 

169 

170 # Break, if amount of accepted_keys are more than threshold. 

171 if len(accepted_keys) >= threshold: 

172 break 

173 

174 if threshold > len(accepted_keys): 

175 raise exceptions.VerificationError( 

176 "Accepted signatures do not match threshold," 

177 f" Found: {len(accepted_keys)}, Expected {threshold}" 

178 ) 

179 

180 return accepted_keys