Skip to content

Commit e2280c3

Browse files
dzolt-4chaindzoltgithub-actions[bot]
authored
feat: acquire cert issuance (#679)
Co-authored-by: Damian <dzoltow@gmail.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 605a81d commit e2280c3

12 files changed

+1041
-4
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
package actions
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/base64"
7+
"encoding/hex"
8+
"encoding/json"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
13+
"github.com/bsv-blockchain/go-sdk/auth/brc104"
14+
"github.com/bsv-blockchain/go-sdk/auth/certificates"
15+
ec "github.com/bsv-blockchain/go-sdk/primitives/ec"
16+
"github.com/bsv-blockchain/go-sdk/transaction"
17+
sdk "github.com/bsv-blockchain/go-sdk/wallet"
18+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wallet/internal/mapping"
19+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wallet/internal/utils"
20+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wdk"
21+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wdk/primitives"
22+
"github.com/go-softwarelab/common/pkg/to"
23+
)
24+
25+
// ProtocolIssuanceRequest represents the certificate signing request sent to the certifier
26+
// as part of the issuance protocol.
27+
type ProtocolIssuanceRequest struct {
28+
Type string `json:"type"`
29+
Nonce string `json:"clientNonce"`
30+
Fields map[string]string `json:"fields"`
31+
MasterKeyring map[string]string `json:"masterKeyring"`
32+
}
33+
34+
// ProtocolIssuanceResponse represents the response from the certifier containing the signed certificate
35+
// and server nonce for verification.
36+
type ProtocolIssuanceResponse struct {
37+
Protocol string `json:"protocol"`
38+
Certificate *Certificate `json:"certificate"`
39+
ServerNonce string `json:"serverNonce"`
40+
Timestamp string `json:"timestamp"`
41+
Version string `json:"version"`
42+
}
43+
44+
// Certificate represents a certificate as returned by the certifier in the issuance protocol response.
45+
type Certificate struct {
46+
Type string `json:"type"`
47+
SerialNumber string `json:"serialNumber"`
48+
Subject string `json:"subject"`
49+
Certifier string `json:"certifier"`
50+
RevocationOutpoint string `json:"revocationOutpoint"`
51+
Fields map[string]string `json:"fields"`
52+
Signature string `json:"signature"`
53+
}
54+
55+
// PrepareIssuanceActionDataParams contains parameters for preparing the certificate issuance request
56+
type PrepareIssuanceActionDataParams struct {
57+
Wallet sdk.Interface
58+
Args sdk.AcquireCertificateArgs
59+
Nonce []byte
60+
IdentityKey *ec.PublicKey
61+
}
62+
63+
// PrepareIssuanceActionDataResult contains the prepared payload and related data
64+
type PrepareIssuanceActionDataResult struct {
65+
Body []byte
66+
Fields map[string]string
67+
MasterKeyring map[string]string
68+
CertTypeB64 string
69+
CounterParty sdk.Counterparty
70+
}
71+
72+
// ParseCertificateResponseParams contains parameters for parsing the certificate response
73+
type ParseCertificateResponseParams struct {
74+
Response *http.Response
75+
Args sdk.AcquireCertificateArgs
76+
Nonce []byte
77+
IdentityKey *ec.PublicKey
78+
}
79+
80+
// ParseCertificateResponseResult contains the parsed and validated certificate
81+
type ParseCertificateResponseResult struct {
82+
Certificate *certificates.Certificate
83+
ParsedCertifier *ec.PublicKey
84+
RevocationOutpoint *transaction.Outpoint
85+
ParsedSignature *ec.Signature
86+
SerialNumber []byte
87+
ServerNonce string
88+
CertFields map[sdk.CertificateFieldNameUnder50Bytes]sdk.StringBase64
89+
}
90+
91+
// StoreCertificateParams contains parameters for storing the certificate
92+
type StoreCertificateParams struct {
93+
Storage AcquireCertificateIssuanceStorage
94+
Auth wdk.AuthID
95+
Certificate *certificates.Certificate
96+
Certifier *ec.PublicKey
97+
RevocationOutpoint *transaction.Outpoint
98+
Signature *ec.Signature
99+
IdentityKey *ec.PublicKey
100+
CertTypeB64 string
101+
Fields map[string]string
102+
MasterKeyring map[string]string
103+
}
104+
105+
// AcquireCertificateIssuanceStorage defines the storage methods needed for certificate issuance
106+
type AcquireCertificateIssuanceStorage interface {
107+
GetAuth(ctx context.Context) (wdk.AuthID, error)
108+
InsertCertificateAuth(ctx context.Context, cert *wdk.TableCertificateX) (uint, error)
109+
}
110+
111+
// PrepareIssuanceActionData creates the certificate signing request payload along with certificate data
112+
func PrepareIssuanceActionData(ctx context.Context, p PrepareIssuanceActionDataParams) (*PrepareIssuanceActionDataResult, error) {
113+
// Prepare counterparty for encryption
114+
counterParty := sdk.Counterparty{
115+
Counterparty: p.Args.Certifier,
116+
Type: sdk.CounterpartyTypeOther,
117+
}
118+
119+
// Create encrypted certificate fields
120+
fieldsForEncryption, err := mapping.MapToFieldsForEncryption(p.Args.Fields)
121+
if err != nil {
122+
return nil, fmt.Errorf("failed to map fields to fields for encryption: %w", err)
123+
}
124+
125+
certificateFieldsResult, err := certificates.CreateCertificateFields(ctx, p.Wallet, counterParty, fieldsForEncryption, to.Value(p.Args.Privileged), p.Args.PrivilegedReason)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to create certificate fields: %w", err)
128+
}
129+
130+
fields := make(map[string]string, len(certificateFieldsResult.CertificateFields))
131+
for k, v := range certificateFieldsResult.CertificateFields {
132+
fields[to.String(k)] = to.String(v)
133+
}
134+
135+
masterKeyring := make(map[string]string, len(certificateFieldsResult.MasterKeyring))
136+
for k, v := range certificateFieldsResult.MasterKeyring {
137+
masterKeyring[to.String(k)] = to.String(v)
138+
}
139+
140+
// Build certificate signing request
141+
certTypeB64 := base64.StdEncoding.EncodeToString(p.Args.Type[:])
142+
body, err := json.Marshal(&ProtocolIssuanceRequest{
143+
Type: certTypeB64,
144+
Nonce: string(p.Nonce),
145+
Fields: fields,
146+
MasterKeyring: masterKeyring,
147+
})
148+
if err != nil {
149+
return nil, fmt.Errorf("failed to marshal HTTP request payload: %w", err)
150+
}
151+
152+
return &PrepareIssuanceActionDataResult{
153+
Body: body,
154+
Fields: fields,
155+
MasterKeyring: masterKeyring,
156+
CertTypeB64: certTypeB64,
157+
CounterParty: counterParty,
158+
}, nil
159+
}
160+
161+
// ParseCertificateResponse parses and validates the certificate response from the certifier
162+
func ParseCertificateResponse(p ParseCertificateResponseParams) (*ParseCertificateResponseResult, error) {
163+
// Read response body
164+
responseBytes, err := io.ReadAll(p.Response.Body)
165+
if err != nil {
166+
return nil, fmt.Errorf("failed to read response body: %w", err)
167+
}
168+
169+
if p.Response.StatusCode < 200 || p.Response.StatusCode >= 300 {
170+
return nil, fmt.Errorf("unexpected HTTP status code %d: %s", p.Response.StatusCode, string(responseBytes))
171+
}
172+
173+
var response ProtocolIssuanceResponse
174+
if err := json.Unmarshal(responseBytes, &response); err != nil {
175+
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
176+
}
177+
178+
// Validate response headers and required fields
179+
responseAuthHeader := p.Response.Header.Get(brc104.HeaderIdentityKey)
180+
if responseAuthHeader != p.Args.Certifier.ToDERHex() {
181+
return nil, fmt.Errorf("invalid certifier! Expected: %s, Received: %s", p.Args.Certifier.ToDERHex(), responseAuthHeader)
182+
}
183+
184+
if response.ServerNonce == "" {
185+
return nil, fmt.Errorf("no serverNonce received from certifier")
186+
}
187+
188+
if response.Certificate == nil {
189+
return nil, fmt.Errorf("no certificate received from certifier")
190+
}
191+
192+
// Parse certificate components
193+
subject, err := ec.PublicKeyFromString(response.Certificate.Subject)
194+
if err != nil {
195+
return nil, fmt.Errorf("failed to parse subject field to public key: %w", err)
196+
}
197+
198+
certifier, err := ec.PublicKeyFromString(response.Certificate.Certifier)
199+
if err != nil {
200+
return nil, fmt.Errorf("failed to parse certifier field to public key: %w", err)
201+
}
202+
203+
revocationOutpoint, err := transaction.OutpointFromString(response.Certificate.RevocationOutpoint)
204+
if err != nil {
205+
return nil, fmt.Errorf("failed to parse revocation outpoint: %w", err)
206+
}
207+
208+
signatureBytes, err := hex.DecodeString(response.Certificate.Signature)
209+
if err != nil {
210+
return nil, fmt.Errorf("failed to decode signature from hex: %w", err)
211+
}
212+
213+
parsedSignature, err := ec.ParseSignature(signatureBytes)
214+
if err != nil {
215+
return nil, fmt.Errorf("failed to parse signature: %w", err)
216+
}
217+
218+
certFields, err := mapping.MapToCertificateFields(response.Certificate.Fields)
219+
if err != nil {
220+
return nil, fmt.Errorf("failed to map certificate fields: %w", err)
221+
}
222+
223+
signedCert := certificates.NewCertificate(
224+
sdk.StringBase64(response.Certificate.Type),
225+
sdk.StringBase64(response.Certificate.SerialNumber),
226+
to.Value(subject),
227+
to.Value(certifier),
228+
revocationOutpoint,
229+
certFields,
230+
signatureBytes)
231+
232+
serialNumber, err := base64.StdEncoding.DecodeString(string(signedCert.SerialNumber))
233+
if err != nil {
234+
return nil, fmt.Errorf("failed to decode serialNumber: %w", err)
235+
}
236+
237+
return &ParseCertificateResponseResult{
238+
Certificate: signedCert,
239+
ParsedCertifier: certifier,
240+
RevocationOutpoint: revocationOutpoint,
241+
ParsedSignature: parsedSignature,
242+
SerialNumber: serialNumber,
243+
ServerNonce: response.ServerNonce,
244+
CertFields: certFields,
245+
}, nil
246+
}
247+
248+
// VerifyCertificateIssuance verifies the certificate against the original request parameters
249+
func VerifyCertificateIssuance(ctx context.Context, wallet sdk.Interface, parsedCert *ParseCertificateResponseResult, nonce []byte, issuanceActionData *PrepareIssuanceActionDataResult, subject, certifier *ec.PublicKey, originator string) error {
250+
// Verify if serial number has correct length
251+
if len(parsedCert.SerialNumber) != utils.NonceHMACSize {
252+
return fmt.Errorf("invalid serialNumber length: got %d, want %d", len(parsedCert.SerialNumber), utils.NonceHMACSize)
253+
}
254+
// Verify HMAC of serial number
255+
dataToVerify := bytes.Join([][]byte{nonce, []byte(parsedCert.ServerNonce)}, []byte{})
256+
var hmacToVerifyArray [32]byte
257+
copy(hmacToVerifyArray[:], parsedCert.SerialNumber)
258+
259+
verifyHmacResult, err := wallet.VerifyHMAC(ctx, sdk.VerifyHMACArgs{
260+
HMAC: hmacToVerifyArray,
261+
Data: dataToVerify,
262+
EncryptionArgs: sdk.EncryptionArgs{
263+
KeyID: parsedCert.ServerNonce + string(nonce),
264+
ProtocolID: sdk.Protocol{
265+
SecurityLevel: sdk.SecurityLevelEveryAppAndCounterparty,
266+
Protocol: "certificate issuance",
267+
},
268+
Counterparty: issuanceActionData.CounterParty,
269+
},
270+
}, originator)
271+
if err != nil {
272+
return fmt.Errorf("failed to verify HMAC signature: %w", err)
273+
}
274+
if !verifyHmacResult.Valid {
275+
return fmt.Errorf("invalid serialNumber")
276+
}
277+
278+
// Validate certificate type
279+
if string(parsedCert.Certificate.Type) != issuanceActionData.CertTypeB64 {
280+
return fmt.Errorf("invalid certificate type! Expected: %s, Received: %s", issuanceActionData.CertTypeB64, parsedCert.Certificate.Type)
281+
}
282+
283+
// Validate certificate subject matches our identity key
284+
if parsedCert.Certificate.Subject.ToDERHex() != subject.ToDERHex() {
285+
return fmt.Errorf("invalid certificate subject! Expected: %s, Received: %s", subject.ToDERHex(), parsedCert.Certificate.Subject.ToDERHex())
286+
}
287+
288+
// Validate certifier
289+
if parsedCert.Certificate.Certifier.ToDERHex() != certifier.ToDERHex() {
290+
return fmt.Errorf("invalid certifier! Expected: %s, Received: %s", certifier.ToDERHex(), parsedCert.Certificate.Certifier.ToDERHex())
291+
}
292+
293+
// Validate revocation outpoint exists
294+
if parsedCert.Certificate.RevocationOutpoint == nil {
295+
return fmt.Errorf("invalid revocationOutpoint")
296+
}
297+
298+
// Validate that certificate fields match what we sent
299+
if len(parsedCert.Certificate.Fields) != len(issuanceActionData.Fields) {
300+
return fmt.Errorf("fields mismatch! Objects have different number of keys. Expected: %d, Received: %d", len(issuanceActionData.Fields), len(parsedCert.Certificate.Fields))
301+
}
302+
303+
for fieldName, fieldValue := range issuanceActionData.Fields {
304+
signedCertFieldValue, isPresent := parsedCert.Certificate.Fields[sdk.CertificateFieldNameUnder50Bytes(fieldName)]
305+
if !isPresent {
306+
return fmt.Errorf("missing field: %s in certificate fields from the certifier", fieldName)
307+
}
308+
309+
if string(signedCertFieldValue) != fieldValue {
310+
return fmt.Errorf("invalid field! Expected: %s, Received: %s", fieldValue, string(signedCertFieldValue))
311+
}
312+
}
313+
314+
// Verify certificate signature
315+
if err := parsedCert.Certificate.Verify(ctx); err != nil {
316+
return fmt.Errorf("failed to verify certificate: %w", err)
317+
}
318+
319+
return nil
320+
}
321+
322+
// TestCertificateDecryption verifies that the certificate fields can be decrypted
323+
func TestCertificateDecryption(ctx context.Context, wallet sdk.Interface, certFields map[sdk.CertificateFieldNameUnder50Bytes]sdk.StringBase64, masterKeyring map[string]string, counterParty sdk.Counterparty, args sdk.AcquireCertificateArgs) error {
324+
certMasterKeyring, err := mapping.MapToCertificateFields(masterKeyring)
325+
if err != nil {
326+
return fmt.Errorf("failed to map certificate master keyring fields: %w", err)
327+
}
328+
329+
_, err = certificates.DecryptFields(ctx, wallet,
330+
certMasterKeyring,
331+
certFields,
332+
counterParty,
333+
to.Value(args.Privileged),
334+
args.PrivilegedReason,
335+
)
336+
if err != nil {
337+
return fmt.Errorf("failed to decrypt certificate: %w", err)
338+
}
339+
340+
return nil
341+
}
342+
343+
// StoreCertificate saves the certificate to storage
344+
func StoreCertificate(ctx context.Context, p StoreCertificateParams) error {
345+
// Convert signature to hex string for storage
346+
rHex := fmt.Sprintf("%064x", p.Signature.R)
347+
sHex := fmt.Sprintf("%064x", p.Signature.S)
348+
sigHex := rHex + sHex
349+
350+
// Parse fields into TableCertificateField slice
351+
certificateFields, err := wdk.ParseToTableCertificateFieldSlice(to.Value(p.Auth.UserID), p.Fields, p.MasterKeyring)
352+
if err != nil {
353+
return fmt.Errorf("failed to parse certificate fields for user %d: %w", to.Value(p.Auth.UserID), err)
354+
}
355+
356+
// Insert certificate into storage
357+
certifierPubKeyHex := primitives.PubKeyHex(p.Certifier.ToDERHex())
358+
_, err = p.Storage.InsertCertificateAuth(ctx, &wdk.TableCertificateX{
359+
TableCertificate: wdk.TableCertificate{
360+
UserID: to.Value(p.Auth.UserID),
361+
Type: primitives.Base64String(p.CertTypeB64),
362+
SerialNumber: primitives.Base64String(p.Certificate.SerialNumber),
363+
Certifier: certifierPubKeyHex,
364+
Subject: primitives.PubKeyHex(p.IdentityKey.ToDERHex()),
365+
RevocationOutpoint: primitives.OutpointString(p.RevocationOutpoint.String()),
366+
Signature: primitives.HexString(sigHex),
367+
Verifier: to.Ptr(certifierPubKeyHex), // Certifier is the verifier (KeyringRevealer.Certifier is true)
368+
},
369+
Fields: certificateFields,
370+
})
371+
if err != nil {
372+
return fmt.Errorf("failed to insert certificate for user %d: %w", to.Value(p.Auth.UserID), err)
373+
}
374+
375+
return nil
376+
}

0 commit comments

Comments
 (0)