Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/securesystemslib/signer/_sigstore_signer.py: 29%

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

122 statements  

1"""Signer implementation for project sigstore.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import logging 

7from typing import Any 

8from urllib import parse 

9 

10from securesystemslib.exceptions import ( 

11 UnsupportedLibraryError, 

12 UnverifiedSignatureError, 

13 VerificationError, 

14) 

15from securesystemslib.signer._signer import ( 

16 Key, 

17 SecretsHandler, 

18 Signature, 

19 Signer, 

20) 

21from securesystemslib.signer._utils import compute_default_keyid 

22 

23IMPORT_ERROR = "sigstore library required to use 'sigstore-oidc' keys" 

24 

25# ruff: noqa: PLC0415 

26 

27logger = logging.getLogger(__name__) 

28 

29 

30class SigstoreKey(Key): 

31 """Sigstore verifier. 

32 

33 NOTE: The Sigstore key and signature serialization formats are not yet 

34 considered stable in securesystemslib. They may change in future releases 

35 and may not be supported by other implementations. 

36 """ 

37 

38 DEFAULT_KEY_TYPE = "sigstore-oidc" 

39 DEFAULT_SCHEME = "Fulcio" 

40 

41 def __init__( 

42 self, 

43 keyid: str, 

44 keytype: str, 

45 scheme: str, 

46 keyval: dict[str, Any], 

47 unrecognized_fields: dict[str, Any] | None = None, 

48 ): 

49 for content in ["identity", "issuer"]: 

50 if content not in keyval or not isinstance(keyval[content], str): 

51 raise ValueError(f"{content} string required for scheme {scheme}") 

52 super().__init__(keyid, keytype, scheme, keyval, unrecognized_fields) 

53 

54 @classmethod 

55 def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> SigstoreKey: 

56 keytype, scheme, keyval = cls._from_dict(key_dict) 

57 return cls(keyid, keytype, scheme, keyval, key_dict) 

58 

59 def to_dict(self) -> dict: 

60 return self._to_dict() 

61 

62 def verify_signature(self, signature: Signature, data: bytes) -> None: 

63 try: 

64 from sigstore.errors import VerificationError as SigstoreVerifyError 

65 from sigstore.models import Bundle 

66 from sigstore.verify import Verifier 

67 from sigstore.verify.policy import Identity 

68 except ImportError as e: 

69 raise VerificationError(IMPORT_ERROR) from e 

70 

71 try: 

72 verifier = Verifier.production() 

73 identity = Identity( 

74 identity=self.keyval["identity"], issuer=self.keyval["issuer"] 

75 ) 

76 bundle_data = signature.unrecognized_fields["bundle"] 

77 bundle = Bundle.from_json(json.dumps(bundle_data)) 

78 

79 verifier.verify_artifact(data, bundle, identity) 

80 

81 except SigstoreVerifyError as e: 

82 logger.info( 

83 "Key %s failed to verify sig: %s", 

84 self.keyid, 

85 e, 

86 ) 

87 raise UnverifiedSignatureError( 

88 f"Failed to verify signature by {self.keyid}" 

89 ) from e 

90 except Exception as e: 

91 logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) 

92 raise VerificationError( 

93 f"Unknown failure to verify signature by {self.keyid}" 

94 ) from e 

95 

96 

97class SigstoreSigner(Signer): 

98 """Sigstore signer. 

99 

100 NOTE: The Sigstore key and signature serialization formats are not yet 

101 considered stable in securesystemslib. They may change in future releases 

102 and may not be supported by other implementations. 

103 

104 All signers should be instantiated with ``Signer.from_priv_key_uri()``. 

105 Unstable ``SigstoreSigner`` currently requires opt-in via 

106 ``securesystemslib.signer.SIGNER_FOR_URI_SCHEME``. 

107 

108 Usage:: 

109 

110 identity = "luk.puehringer@gmail.com" # change, unless you know pw 

111 issuer = "https://github.com/login/oauth" 

112 

113 # Create signer URI and public key for identity and issuer 

114 uri, public_key = SigstoreSigner.import_(identity, issuer, ambient=False) 

115 

116 # Load signer from URI -- requires browser login with GitHub 

117 signer = SigstoreSigner.from_priv_key_uri(uri, public_key) 

118 

119 # Sign with signer and verify public key 

120 signature = signer.sign(b"data") 

121 public_key.verify_signature(signature, b"data") 

122 

123 The private key URI scheme is ``sigstore:?<PARAMS>``, where PARAMS is 

124 optional and toggles ambient credential usage. Example URIs: 

125 

126 * ``sigstore:``: 

127 Sign with ambient credentials. 

128 * ``sigstore:?ambient=false``: 

129 Sign with OAuth2 + OpenID via browser login. 

130 

131 Raises: 

132 UnsupportedLibraryError: If sigstore library is not installed. 

133 """ 

134 

135 SCHEME = "sigstore" 

136 

137 def __init__(self, token: Any, public_key: Key): 

138 self._public_key = public_key 

139 # token is of type sigstore.oidc.IdentityToken but the module should be usable 

140 # without sigstore so it's not annotated 

141 self._token = token 

142 

143 @property 

144 def public_key(self) -> Key: 

145 return self._public_key 

146 

147 @classmethod 

148 def from_priv_key_uri( 

149 cls, 

150 priv_key_uri: str, 

151 public_key: Key, 

152 secrets_handler: SecretsHandler | None = None, 

153 ) -> SigstoreSigner: 

154 try: 

155 from sigstore.models import ClientTrustConfig 

156 from sigstore.oidc import IdentityToken, Issuer, detect_credential 

157 except ImportError as e: 

158 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

159 

160 if not isinstance(public_key, SigstoreKey): 

161 raise ValueError(f"expected SigstoreKey for {priv_key_uri}") 

162 

163 uri = parse.urlparse(priv_key_uri) 

164 

165 if uri.scheme != cls.SCHEME: 

166 raise ValueError(f"SigstoreSigner does not support {priv_key_uri}") 

167 

168 params = dict(parse.parse_qsl(uri.query)) 

169 ambient = params.get("ambient", "true") == "true" 

170 

171 if not ambient: 

172 # TODO: Restrict oauth flow to use identity/issuer from public_key 

173 # TODO: Use secrets_handler for identity_token() secret arg 

174 trust_config = ClientTrustConfig.production() 

175 issuer = Issuer(trust_config.signing_config.get_oidc_url()) 

176 token = issuer.identity_token() 

177 else: 

178 credential = detect_credential() 

179 if not credential: 

180 raise RuntimeError("Failed to detect Sigstore credentials") 

181 token = IdentityToken(credential) 

182 

183 key_identity = public_key.keyval["identity"] 

184 key_issuer = public_key.keyval["issuer"] 

185 if key_issuer != token.federated_issuer: 

186 raise ValueError( 

187 f"Signer identity issuer {token.federated_issuer} " 

188 f"did not match key: {key_issuer}" 

189 ) 

190 # TODO: should check ambient identity too: unfortunately IdentityToken does 

191 # not provide access to the expected identity value (cert SAN) in ambient case 

192 if not ambient and key_identity != token.identity: 

193 raise ValueError( 

194 f"Signer identity {token.identity} did not match key: {key_identity}" 

195 ) 

196 

197 return cls(token, public_key) 

198 

199 @classmethod 

200 def _get_uri(cls, ambient: bool) -> str: 

201 return f"{cls.SCHEME}:{'' if ambient else '?ambient=false'}" 

202 

203 @classmethod 

204 def import_( 

205 cls, identity: str, issuer: str, ambient: bool = True 

206 ) -> tuple[str, SigstoreKey]: 

207 """Create public key and signer URI. 

208 

209 Returns a private key URI (for Signer.from_priv_key_uri()) and a public 

210 key. import_() should be called once and the returned URI and public 

211 key should be stored for later use. 

212 

213 Arguments: 

214 identity: The OIDC identity to use when verifying a signature. 

215 issuer: The OIDC issuer to use when verifying a signature. 

216 ambient: Toggle usage of ambient credentials in returned URI. 

217 """ 

218 keytype = SigstoreKey.DEFAULT_KEY_TYPE 

219 scheme = SigstoreKey.DEFAULT_SCHEME 

220 keyval = {"identity": identity, "issuer": issuer} 

221 keyid = compute_default_keyid(keytype, scheme, keyval) 

222 key = SigstoreKey(keyid, keytype, scheme, keyval) 

223 uri = cls._get_uri(ambient) 

224 

225 return uri, key 

226 

227 @classmethod 

228 def import_via_auth(cls) -> tuple[str, SigstoreKey]: 

229 """Create public key and signer URI by interactive authentication 

230 

231 Returns a private key URI (for Signer.from_priv_key_uri()) and a public 

232 key. This method always uses the interactive authentication. 

233 """ 

234 try: 

235 from sigstore.models import ClientTrustConfig 

236 from sigstore.oidc import Issuer 

237 except ImportError as e: 

238 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

239 

240 # authenticate to get the identity and issuer 

241 trust_config = ClientTrustConfig.production() 

242 issuer = Issuer(trust_config.signing_config.get_oidc_url()) 

243 token = issuer.identity_token() 

244 return cls.import_(token.identity, token.federated_issuer, False) 

245 

246 def sign(self, payload: bytes) -> Signature: 

247 """Signs payload using the OIDC token on the signer instance. 

248 

249 Arguments: 

250 payload: bytes to be signed. 

251 

252 Raises: 

253 Various errors from sigstore-python. 

254 

255 Returns: 

256 Signature. 

257 

258 NOTE: The relevant data is in `unrecognized_fields["bundle"]`. 

259 

260 """ 

261 try: 

262 from sigstore.models import ClientTrustConfig 

263 from sigstore.sign import SigningContext 

264 except ImportError as e: 

265 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

266 

267 context = SigningContext.from_trust_config(ClientTrustConfig.production()) 

268 with context.signer(self._token) as sigstore_signer: 

269 bundle = sigstore_signer.sign_artifact(payload) 

270 # We want to access the actual signature, see 

271 # https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto 

272 bundle_json = json.loads(bundle.to_json()) 

273 return Signature( 

274 self.public_key.keyid, 

275 bundle_json["messageSignature"]["signature"], 

276 {"bundle": bundle_json}, 

277 ) 

278 

279 @classmethod 

280 def import_github_actions( 

281 cls, project: str, workflow_path: str, ref: str | None = "refs/heads/main" 

282 ) -> tuple[str, SigstoreKey]: 

283 """Convenience method to build identity and issuer string for import_() from 

284 GitHub project and workflow path. 

285 

286 Args: 

287 project: GitHub project name (example: 

288 "secure-systems-lab/securesystemslib") 

289 workflow_path: GitHub workflow path (example: 

290 ".github/workflows/online-sign.yml") 

291 ref: optional GitHub ref, defaults to refs/heads/main 

292 

293 Returns: 

294 uri: string 

295 key: SigstoreKey 

296 

297 """ 

298 identity = f"https://github.com/{project}/{workflow_path}@{ref}" 

299 issuer = "https://token.actions.githubusercontent.com" 

300 uri, key = cls.import_(identity, issuer) 

301 

302 return uri, key