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

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

115 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 

25logger = logging.getLogger(__name__) 

26 

27 

28class SigstoreKey(Key): 

29 """Sigstore verifier. 

30 

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

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

33 and may not be supported by other implementations. 

34 """ 

35 

36 DEFAULT_KEY_TYPE = "sigstore-oidc" 

37 DEFAULT_SCHEME = "Fulcio" 

38 

39 def __init__( 

40 self, 

41 keyid: str, 

42 keytype: str, 

43 scheme: str, 

44 keyval: dict[str, Any], 

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

46 ): 

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

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

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

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

51 

52 @classmethod 

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

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

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

56 

57 def to_dict(self) -> dict: 

58 return self._to_dict() 

59 

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

61 try: 

62 from sigstore.errors import VerificationError as SigstoreVerifyError 

63 from sigstore.models import Bundle 

64 from sigstore.verify import Verifier 

65 from sigstore.verify.policy import Identity 

66 except ImportError as e: 

67 raise VerificationError(IMPORT_ERROR) from e 

68 

69 try: 

70 verifier = Verifier.production() 

71 identity = Identity( 

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

73 ) 

74 bundle_data = signature.unrecognized_fields["bundle"] 

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

76 

77 verifier.verify_artifact(data, bundle, identity) 

78 

79 except SigstoreVerifyError as e: 

80 logger.info( 

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

82 self.keyid, 

83 e, 

84 ) 

85 raise UnverifiedSignatureError( 

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

87 ) from e 

88 except Exception as e: 

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

90 raise VerificationError( 

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

92 ) from e 

93 

94 

95class SigstoreSigner(Signer): 

96 """Sigstore signer. 

97 

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

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

100 and may not be supported by other implementations. 

101 

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

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

104 ``securesystemslib.signer.SIGNER_FOR_URI_SCHEME``. 

105 

106 Usage:: 

107 

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

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

110 

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

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

113 

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

115 signer = SigstoreSigner.from_priv_key_uri(uri, public_key) 

116 

117 # Sign with signer and verify public key 

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

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

120 

121 The private key URI scheme is "sigstore:?<PARAMS>", where PARAMS is 

122 optional and toggles ambient credential usage. Example URIs: 

123 

124 * "sigstore:": 

125 Sign with ambient credentials. 

126 * "sigstore:?ambient=false": 

127 Sign with OAuth2 + OpenID via browser login. 

128 

129 Arguments: 

130 token: The OIDC identity token used for signing. 

131 public_key: The related public key instance. 

132 

133 Raises: 

134 UnsupportedLibraryError: sigstore library not found. 

135 """ 

136 

137 SCHEME = "sigstore" 

138 

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

140 self._public_key = public_key 

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

142 # without sigstore so it's not annotated 

143 self._token = token 

144 

145 @property 

146 def public_key(self) -> Key: 

147 return self._public_key 

148 

149 @classmethod 

150 def from_priv_key_uri( 

151 cls, 

152 priv_key_uri: str, 

153 public_key: Key, 

154 secrets_handler: SecretsHandler | None = None, 

155 ) -> SigstoreSigner: 

156 try: 

157 from sigstore.oidc import IdentityToken, Issuer, detect_credential 

158 except ImportError as e: 

159 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

160 

161 if not isinstance(public_key, SigstoreKey): 

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

163 

164 uri = parse.urlparse(priv_key_uri) 

165 

166 if uri.scheme != cls.SCHEME: 

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

168 

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

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

171 

172 if not ambient: 

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

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

175 token = Issuer.production().identity_token() 

176 else: 

177 credential = detect_credential() 

178 if not credential: 

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

180 token = IdentityToken(credential) 

181 

182 key_identity = public_key.keyval["identity"] 

183 key_issuer = public_key.keyval["issuer"] 

184 if key_issuer != token.federated_issuer: 

185 raise ValueError( 

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

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

188 ) 

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

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

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

192 raise ValueError( 

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

194 ) 

195 

196 return cls(token, public_key) 

197 

198 @classmethod 

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

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

201 

202 @classmethod 

203 def import_( 

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

205 ) -> tuple[str, SigstoreKey]: 

206 """Create public key and signer URI. 

207 

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

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

210 key should be stored for later use. 

211 

212 Arguments: 

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

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

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

216 """ 

217 keytype = SigstoreKey.DEFAULT_KEY_TYPE 

218 scheme = SigstoreKey.DEFAULT_SCHEME 

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

220 keyid = compute_default_keyid(keytype, scheme, keyval) 

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

222 uri = cls._get_uri(ambient) 

223 

224 return uri, key 

225 

226 @classmethod 

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

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

229 

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

231 key. This method always uses the interactive authentication. 

232 """ 

233 try: 

234 from sigstore.oidc import Issuer 

235 except ImportError as e: 

236 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

237 

238 # authenticate to get the identity and issuer 

239 token = Issuer.production().identity_token() 

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

241 

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

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

244 

245 Arguments: 

246 payload: bytes to be signed. 

247 

248 Raises: 

249 Various errors from sigstore-python. 

250 

251 Returns: 

252 Signature. 

253 

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

255 

256 """ 

257 try: 

258 from sigstore.sign import SigningContext 

259 except ImportError as e: 

260 raise UnsupportedLibraryError(IMPORT_ERROR) from e 

261 

262 context = SigningContext.production() 

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

264 bundle = sigstore_signer.sign_artifact(payload) 

265 # We want to access the actual signature, see 

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

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

268 return Signature( 

269 self.public_key.keyid, 

270 bundle_json["messageSignature"]["signature"], 

271 {"bundle": bundle_json}, 

272 ) 

273 

274 @classmethod 

275 def import_github_actions( 

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

277 ) -> tuple[str, SigstoreKey]: 

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

279 GitHub project and workflow path. 

280 

281 Args: 

282 project: GitHub project name (example: 

283 "secure-systems-lab/securesystemslib") 

284 workflow_path: GitHub workflow path (example: 

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

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

287 

288 Returns: 

289 uri: string 

290 key: SigstoreKey 

291 

292 """ 

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

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

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

296 

297 return uri, key