Skip to content

Commit 714a927

Browse files
committed
feat: get sigstore working
1 parent caead6f commit 714a927

9 files changed

Lines changed: 254 additions & 24 deletions

File tree

crates/auths-cli/src/bin/sign.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,8 @@ mod tests {
428428
]);
429429

430430
let data = b"test data to sign";
431-
let result = auths_sdk::crypto::create_sshsig(&seed, data, "git");
431+
let result =
432+
auths_sdk::crypto::create_sshsig(&seed, data, "git", auths_crypto::CurveType::Ed25519);
432433

433434
assert!(result.is_ok(), "SSHSIG creation failed: {:?}", result.err());
434435

crates/auths-core/src/crypto/ssh/signatures.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,12 @@ pub fn create_sshsig(
2727
seed: &SecureSeed,
2828
data: &[u8],
2929
namespace: &str,
30+
curve: auths_crypto::CurveType,
3031
) -> Result<String, CryptoError> {
31-
// Detect curve from the stored key context.
32-
// For now, try Ed25519 first (most common), then P-256.
33-
// TODO: pass CurveType explicitly once the full signing path is curve-aware.
34-
if let Ok(pem) = create_sshsig_ed25519(seed, data, namespace) {
35-
return Ok(pem);
32+
match curve {
33+
auths_crypto::CurveType::Ed25519 => create_sshsig_ed25519(seed, data, namespace),
34+
auths_crypto::CurveType::P256 => create_sshsig_p256(seed, data, namespace),
3635
}
37-
create_sshsig_p256(seed, data, namespace)
3836
}
3937

4038
fn create_sshsig_ed25519(

crates/auths-core/tests/cases/ssh_crypto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ fn test_create_sshsig_returns_pem() {
2323
0x1f, 0x20,
2424
]);
2525

26-
let pem = create_sshsig(&seed, b"test data", "git").unwrap();
26+
let pem = create_sshsig(&seed, b"test data", "git", auths_crypto::CurveType::Ed25519).unwrap();
2727
assert!(pem.starts_with("-----BEGIN SSH SIGNATURE-----"));
2828
assert!(pem.contains("-----END SSH SIGNATURE-----"));
2929
}

crates/auths-sdk/src/domains/signing/service.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,10 @@ pub fn sign_with_seed(
176176
seed: &SecureSeed,
177177
data: &[u8],
178178
namespace: &str,
179+
curve: auths_crypto::CurveType,
179180
) -> Result<String, SigningError> {
180-
ssh::create_sshsig(seed, data, namespace).map_err(|e| SigningError::PemEncoding(e.to_string()))
181+
ssh::create_sshsig(seed, data, namespace, curve)
182+
.map_err(|e| SigningError::PemEncoding(e.to_string()))
181183
}
182184

183185
// ---------------------------------------------------------------------------

crates/auths-sdk/src/workflows/signing.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use chrono::{DateTime, Utc};
1111

1212
use auths_core::AgentError;
1313
use auths_core::crypto::signer::decrypt_keypair;
14-
use auths_core::crypto::ssh::{SecureSeed, extract_seed_from_pkcs8};
14+
use auths_core::crypto::ssh::SecureSeed;
1515
use auths_core::signing::PassphraseProvider;
1616
use auths_core::storage::keychain::{KeyAlias, KeyStorage};
1717
use auths_crypto::Pkcs8Der;
@@ -163,16 +163,17 @@ impl CommitSigningWorkflow {
163163
let _ = ctx.agent_signing.ensure_running();
164164

165165
let pkcs8 = load_key_with_passphrase_retry(ctx, &params)?;
166-
let seed = extract_seed_from_pkcs8(&pkcs8)
167-
.map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
166+
let (seed, _pubkey, curve) =
167+
auths_core::crypto::signer::load_seed_and_pubkey(pkcs8.as_ref())
168+
.map_err(|e| SigningError::KeyDecryptionFailed(e.to_string()))?;
168169

169170
// Best-effort: load identity into agent for future Tier 1 hits
170171
let _ = ctx
171172
.agent_signing
172173
.add_identity(&params.namespace, pkcs8.as_ref());
173174

174175
// Tier 3: direct sign
175-
direct_sign(&params, &seed, now)
176+
direct_sign(&params, &seed, now, curve)
176177
}
177178
}
178179

@@ -231,10 +232,11 @@ fn direct_sign(
231232
params: &CommitSigningParams,
232233
seed: &SecureSeed,
233234
now: DateTime<Utc>,
235+
curve: auths_crypto::CurveType,
234236
) -> Result<String, SigningError> {
235237
if let Some(ref repo_path) = params.repo_path {
236238
signing::validate_freeze_state(repo_path, now)?;
237239
}
238240

239-
signing::sign_with_seed(seed, &params.data, &params.namespace)
241+
signing::sign_with_seed(seed, &params.data, &params.namespace, curve)
240242
}

crates/auths-sdk/tests/cases/signing.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ fn test_sign_with_known_seed() {
3636
0x1f, 0x20,
3737
]);
3838

39-
let pem = signing::sign_with_seed(&seed, b"test data", "git").unwrap();
39+
let pem = signing::sign_with_seed(&seed, b"test data", "git", auths_crypto::CurveType::Ed25519)
40+
.unwrap();
4041
assert!(pem.starts_with("-----BEGIN SSH SIGNATURE-----"));
4142
assert!(pem.contains("-----END SSH SIGNATURE-----"));
4243
}

docs/contributing/adding_curves.md

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Adding New Curves
2+
3+
Auths is designed so that adding a new elliptic curve (or a post-quantum algorithm) is a matter of adding enum variants and letting the compiler tell you what's missing. No grep. No guessing. The curve type is carried explicitly through every layer — from key generation to Sigstore submission.
4+
5+
This document explains the current architecture, what's already supported, and the exact steps to add a new curve.
6+
7+
## Currently supported
8+
9+
| Curve | Key size | Signature size | Default | Crate |
10+
|-------|----------|----------------|---------|-------|
11+
| Ed25519 | 32 bytes | 64 bytes | No | `ring` |
12+
| P-256 (secp256r1) | 33 bytes (compressed) | 64 bytes (r‖s) | **Yes** | `p256` |
13+
14+
P-256 is the default for all operations: identity keys, ephemeral CI keys, SSH commit signing, and Sigstore submission. Ed25519 is available for compatibility. We default to P-256 as it is the same default Sigstore uses, so is useful for bootstrapping.
15+
16+
## Architecture: how curves flow through the system
17+
18+
The core design principle: **the curve is an explicit field, never inferred from key length.** This means adding a new curve that shares a byte length with an existing one (e.g., secp256k1 is also 33 bytes compressed, same as P-256) won't break anything.
19+
20+
### Layer 0: `auths-crypto`
21+
22+
This is where a new curve starts. Three types carry the curve:
23+
24+
**`CurveType` enum** (`crates/auths-crypto/src/provider.rs`):
25+
```rust
26+
pub enum CurveType {
27+
Ed25519,
28+
#[default]
29+
P256,
30+
// Add new variant here → compiler errors everywhere it's not handled
31+
}
32+
```
33+
34+
**`TypedSeed` enum** (`crates/auths-crypto/src/key_ops.rs`):
35+
```rust
36+
pub enum TypedSeed {
37+
Ed25519([u8; 32]),
38+
P256([u8; 32]),
39+
// Add new variant here
40+
}
41+
```
42+
43+
**`DecodedDidKey` enum** (`crates/auths-crypto/src/did_key.rs`):
44+
```rust
45+
pub enum DecodedDidKey {
46+
Ed25519([u8; 32]),
47+
P256(Vec<u8>),
48+
// Add new variant here
49+
}
50+
```
51+
52+
All three are exhaustive `match` targets. Adding a variant to any of them produces compiler errors at every dispatch site that doesn't handle the new curve. This is the core mechanism — the compiler is the migration tool.
53+
54+
### Layer 0.5: `auths-keri`
55+
56+
**`KeriPublicKey` enum** (`crates/auths-keri/src/keys.rs`):
57+
```rust
58+
pub enum KeriPublicKey {
59+
Ed25519([u8; 32]),
60+
P256([u8; 33]),
61+
// Add new variant with CESR derivation code
62+
}
63+
```
64+
65+
Each variant has a CESR derivation code prefix (`D` for Ed25519, `1AAJ` for P-256). The new curve needs a CESR code — either from the CESR spec or a private-use code.
66+
67+
### Layer 1: `auths-verifier`
68+
69+
**`DevicePublicKey`** (`crates/auths-verifier/src/core.rs`):
70+
```rust
71+
pub struct DevicePublicKey {
72+
curve: CurveType, // ← carried explicitly
73+
bytes: Vec<u8>,
74+
}
75+
```
76+
77+
Validation is per-curve in `try_new()`:
78+
```rust
79+
let valid = match curve {
80+
CurveType::Ed25519 => bytes.len() == 32,
81+
CurveType::P256 => bytes.len() == 33 || bytes.len() == 65,
82+
// Add new curve's valid lengths here
83+
};
84+
```
85+
86+
### Layer 2+: SSH, Sigstore, CLI
87+
88+
Each layer dispatches on `CurveType` or `TypedSeed`. The compiler forces you to handle the new variant at every site.
89+
90+
## Steps to add a new curve
91+
92+
This is a concrete checklist. The order matters — each step unblocks the next.
93+
94+
### Step 1: Add the crypto primitives
95+
96+
**File: `crates/auths-crypto/src/provider.rs`**
97+
98+
1. Add a variant to `CurveType`:
99+
```rust
100+
pub enum CurveType {
101+
Ed25519,
102+
#[default]
103+
P256,
104+
NewCurve, // e.g., MlDsa44 for post-quantum
105+
}
106+
```
107+
2. Add constants: `NEW_CURVE_PUBLIC_KEY_LEN`, `NEW_CURVE_SIGNATURE_LEN`
108+
3. Update `CurveType::public_key_len()` and `CurveType::signature_len()`
109+
4. Update `Display` impl
110+
111+
**File: `crates/auths-crypto/src/key_ops.rs`**
112+
113+
1. Add a variant to `TypedSeed`:
114+
```rust
115+
pub enum TypedSeed {
116+
Ed25519([u8; 32]),
117+
P256([u8; 32]),
118+
NewCurve([u8; N]), // whatever the seed size is
119+
}
120+
```
121+
2. Update `TypedSeed::curve()`, `TypedSeed::as_bytes()`
122+
3. Add a parsing branch in `parse_key_material()` — detect the new PKCS8 OID or key format
123+
4. Add signing logic in `sign()` — dispatch to the new crate
124+
5. Add public key derivation in `public_key()`
125+
126+
**File: `crates/auths-crypto/src/ring_provider.rs`** (or a new provider file)
127+
128+
Add standalone `new_curve_sign()`, `new_curve_verify()`, `new_curve_public_key_from_seed()` functions.
129+
130+
### Step 2: Add DID encoding
131+
132+
**File: `crates/auths-crypto/src/did_key.rs`**
133+
134+
1. Define the multicodec prefix bytes for the new curve (from the [multicodec table](https://github.com/multiformats/multicodec/blob/master/table.csv))
135+
2. Add `new_curve_pubkey_to_did_key()` function
136+
3. Add variant to `DecodedDidKey`
137+
4. Update `did_key_decode()` to handle the new multicodec prefix
138+
139+
### Step 3: Add KERI support
140+
141+
**File: `crates/auths-keri/src/keys.rs`**
142+
143+
1. Add variant to `KeriPublicKey`
144+
2. Assign a CESR derivation code (check the [CESR spec](https://weboftrust.github.io/ietf-cesr/draft-ssmith-cesr.html) for registered codes)
145+
3. Update `KeriPublicKey::parse()` to detect the new prefix
146+
4. Update `KeriPublicKey::verify_signature()` to dispatch verification
147+
148+
**File: `crates/auths-keri/src/codec.rs`**
149+
150+
1. Add `KeyType::NewCurve` with the CESR code
151+
2. Add `SigType::NewCurve` if the signature format differs
152+
153+
### Step 4: Add KERI inception support
154+
155+
**File: `crates/auths-id/src/keri/inception.rs`**
156+
157+
1. Update `generate_keypair()` to handle the new `CurveType`
158+
2. Update `sign_with_pkcs8()` to handle the new curve's signing
159+
160+
### Step 5: Update DevicePublicKey validation
161+
162+
**File: `crates/auths-verifier/src/core.rs`**
163+
164+
Update `DevicePublicKey::try_new()` to accept the new curve's key lengths.
165+
166+
### Step 6: Add SSH wire format support
167+
168+
**File: `crates/auths-core/src/crypto/ssh/encoding.rs`**
169+
170+
1. Add encoding branch in `encode_ssh_pubkey()` for the new curve's SSH key type string
171+
2. Add encoding branch in `encode_ssh_signature()` for the new curve's SSH signature format
172+
3. Check if SSH has a registered key type for the new curve — post-quantum algorithms may not have one yet
173+
174+
**File: `crates/auths-verifier/src/ssh_sig.rs`**
175+
176+
1. Add parsing branch in `parse_pubkey_blob()` for the new SSH key type string
177+
2. Add parsing branch in `parse_sig_blob()` for the new signature format
178+
179+
### Step 7: Add Sigstore/Rekor support
180+
181+
**File: `crates/auths-infra-rekor/src/client.rs`**
182+
183+
Update `pubkey_to_pem()` to produce the correct SPKI PEM for the new curve. For post-quantum algorithms, check if Rekor's DSSE entry type accepts the key format.
184+
185+
### Step 8: Update Python/Node bindings
186+
187+
**Files: `packages/auths-python/src/sign.rs`, `packages/auths-node/src/sign.rs`**
188+
189+
Update the `curve` parameter parsing to accept the new curve name.
190+
191+
### Step 9: Compile and follow the errors
192+
193+
```bash
194+
cargo check --workspace 2>&1 | grep "non-exhaustive"
195+
```
196+
197+
Every `match` on `CurveType`, `TypedSeed`, `KeriPublicKey`, or `DecodedDidKey` that doesn't handle the new variant will error. Fix each one. This is the compiler doing the migration for you.
198+
199+
### Step 10: Add tests
200+
201+
Each crate uses `tests/cases/` for integration tests. Add test cases for:
202+
- Key generation round-trip (generate → derive pubkey → verify signature)
203+
- KERI inception with the new curve (key prefix starts with the right CESR code)
204+
- DID encoding/decoding round-trip
205+
- SSH signature creation and parsing
206+
- `DevicePublicKey` construction with valid/invalid lengths
207+
208+
## Post-quantum considerations
209+
210+
Post-quantum algorithms (ML-DSA/Dilithium, ML-KEM/Kyber, SLH-DSA/SPHINCS+) have different characteristics:
211+
212+
| Property | Ed25519/P-256 | ML-DSA-44 (Dilithium2) |
213+
|----------|---------------|----------------------|
214+
| Public key | 32-33 bytes | 1,312 bytes |
215+
| Signature | 64 bytes | 2,420 bytes |
216+
| Seed | 32 bytes | 32 bytes |
217+
218+
**Impact on auths:**
219+
220+
- `DevicePublicKey.bytes` is `Vec<u8>` — handles any size
221+
- `TypedSeed` seed size may differ (add a new fixed-size array or use `Vec<u8>`)
222+
- SSH wire format may not have registered key types — may need a custom namespace
223+
- CESR derivation codes for PQ algorithms are not yet standardized
224+
- Attestation JSON size grows significantly — 2KB signatures instead of 64 bytes
225+
- Rekor DSSE entries grow but should still be accepted (well under the 100KB limit)
226+
227+
The architecture handles this. The main work is in the crypto primitives (Step 1) and the wire format registrations (Steps 3, 6). The type-driven dispatch through `CurveType`/`TypedSeed` is curve-agnostic by design.
228+
229+
## What NOT to do
230+
231+
- **Don't infer curve from key length.** That's brittle and breaks when curves share key length. Use `CurveType` everywhere.
232+
- **Don't add a new signing function per curve.** Use `TypedSeed` and dispatch in `key_ops::sign()`.
233+
- **Don't add curve-specific public key types.** Use `DevicePublicKey` with its `curve` field.
234+
- **Don't hardcode key lengths in validation.** Put them in `CurveType::public_key_len()`.

docs/design/transparency-log-port.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,15 +197,6 @@ auths key export --key-alias main --passphrase 'Seamus4444$!' --format pem | ope
197197
rekor-cli search --public-key <base64-der-key> --rekor_server https://rekor.sigstore.dev
198198
```
199199

200-
(auths-e2e-tests) (dev-cleanReadme) me@MacBookPro auths % echo "test artifact" > /tmp/test-artifact.txt
201-
auths artifact sign --log sigstore-rekor /tmp/test-artifact.txt
202-
Logged to sigstore-rekor at index 1271709852
203-
Signed "test-artifact.txt" -> "/tmp/test-artifact.txt.auths.json"
204-
RID: sha256:8308d593eb56527137532595a60255a3fcfbe4b6b068e29b22d99742bad80f6f
205-
Digest: sha256:8308d593eb56527137532595a60255a3fcfbe4b6b068e29b22d99742bad80f6f
206-
207-
echo "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFWGhzL1ZldkNZWWpPL2tCSGN2cmd6TldhR2R3cApJbG05U2IrOUZvMFZEbVRySkVTOHNnbkE2WUFqdUo5ejJocE5aMHM1YjhrUDkvSGNETU5HRTBUSTFRPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" | base64 -d
208-
209200
**What "success" looks like:**
210201
- Step 4 returns the entry without errors
211202
- Step 5 shows `hashedrekord` with `spec.signature.publicKey.content` that decodes to valid SPKI DER

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,4 +538,5 @@ nav:
538538
- Release Process: contributing/release-process.md
539539
- Reference:
540540
- Architecture Decisions: contributing/architecture-decisions.md
541+
- Adding New Curves: contributing/adding_curves.md
541542
- Glossary: contributing/glossary.md

0 commit comments

Comments
 (0)