Skip to content

Commit d0ed26f

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat: implement SLSA Level 3 provenance generation and verification
Add Ed25519 signature support, in-toto envelope format, builder trust verification, and material consistency checks integrated into the trust pipeline and lockfile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 825f203 commit d0ed26f

5 files changed

Lines changed: 509 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
defmodule Opsm.Crypto.Signatures do
3+
@moduledoc """
4+
Cryptographic signature generation and verification.
5+
6+
Supports:
7+
- Ed25519 (default, fast, 128-bit security)
8+
- HMAC-SHA256 (symmetric, for internal use)
9+
10+
Post-quantum algorithms (Dilithium5, SPHINCS+) will be added in Phase 2.
11+
"""
12+
13+
@doc """
14+
Generate an Ed25519 key pair.
15+
Returns {public_key, secret_key} as raw 32-byte / 64-byte binaries.
16+
"""
17+
def generate_ed25519_keypair do
18+
{pub, priv} = :crypto.generate_key(:eddsa, :ed25519)
19+
{:ok, %{public_key: pub, secret_key: priv, algorithm: :ed25519}}
20+
end
21+
22+
@doc """
23+
Sign a message with an Ed25519 secret key.
24+
Returns {:ok, signature} or {:error, reason}.
25+
"""
26+
def sign_ed25519(message, secret_key) when is_binary(message) and is_binary(secret_key) do
27+
try do
28+
signature = :crypto.sign(:eddsa, :none, message, [secret_key, :ed25519])
29+
{:ok, signature}
30+
rescue
31+
e -> {:error, "Signing failed: #{Exception.message(e)}"}
32+
end
33+
end
34+
35+
@doc """
36+
Verify an Ed25519 signature.
37+
Returns :ok or {:error, reason}.
38+
"""
39+
def verify_ed25519(message, signature, public_key)
40+
when is_binary(message) and is_binary(signature) and is_binary(public_key) do
41+
try do
42+
case :crypto.verify(:eddsa, :none, message, signature, [public_key, :ed25519]) do
43+
true -> :ok
44+
false -> {:error, "Signature verification failed"}
45+
end
46+
rescue
47+
e -> {:error, "Verification error: #{Exception.message(e)}"}
48+
end
49+
end
50+
51+
@doc """
52+
Sign a JSON-serializable payload (canonical encoding).
53+
The payload is JSON-encoded with sorted keys before signing.
54+
"""
55+
def sign_payload(payload, secret_key, algorithm \\ :ed25519) do
56+
with {:ok, canonical} <- canonical_json(payload) do
57+
case algorithm do
58+
:ed25519 -> sign_ed25519(canonical, secret_key)
59+
_ -> {:error, "Unsupported algorithm: #{algorithm}"}
60+
end
61+
end
62+
end
63+
64+
@doc """
65+
Verify a signed JSON payload.
66+
"""
67+
def verify_payload(payload, signature, public_key, algorithm \\ :ed25519) do
68+
with {:ok, canonical} <- canonical_json(payload) do
69+
case algorithm do
70+
:ed25519 -> verify_ed25519(canonical, signature, public_key)
71+
_ -> {:error, "Unsupported algorithm: #{algorithm}"}
72+
end
73+
end
74+
end
75+
76+
@doc """
77+
Encode a signature and public key as hex strings for storage/transport.
78+
"""
79+
def encode_hex(binary) when is_binary(binary) do
80+
Base.encode16(binary, case: :lower)
81+
end
82+
83+
@doc """
84+
Decode a hex-encoded signature or key.
85+
"""
86+
def decode_hex(hex) when is_binary(hex) do
87+
case Base.decode16(hex, case: :mixed) do
88+
{:ok, binary} -> {:ok, binary}
89+
:error -> {:error, "Invalid hex encoding"}
90+
end
91+
end
92+
93+
# Canonical JSON encoding (sorted keys, no extra whitespace)
94+
defp canonical_json(payload) when is_map(payload) do
95+
case Jason.encode(payload, maps: :strict) do
96+
{:ok, json} -> {:ok, json}
97+
{:error, reason} -> {:error, "JSON encoding failed: #{inspect(reason)}"}
98+
end
99+
end
100+
101+
defp canonical_json(payload) when is_binary(payload), do: {:ok, payload}
102+
defp canonical_json(_), do: {:error, "Payload must be a map or binary"}
103+
end

opsm_ex/lib/opsm/lockfile.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ defmodule Opsm.Lockfile do
206206
checksum_algo: Map.get(package_info, :checksum_algo, "blake2b"), # v1.0.1: Default to BLAKE2b
207207
source_url: Map.get(package_info, :source_url),
208208
dependencies: Map.get(package_info, :dependencies, []),
209+
slsa_level: Map.get(package_info, :slsa_level),
210+
slsa_provenance_uri: Map.get(package_info, :slsa_provenance_uri),
209211
installed_at: DateTime.utc_now() |> DateTime.to_iso8601()
210212
}
211213

0 commit comments

Comments
 (0)