diff --git a/sdk/experimental/tdf/assertion.go b/sdk/experimental/tdf/assertion.go index db417a5479..b008cf543a 100644 --- a/sdk/experimental/tdf/assertion.go +++ b/sdk/experimental/tdf/assertion.go @@ -3,422 +3,126 @@ package tdf import ( - "encoding/json" - "errors" - "fmt" - - "github.com/gowebpki/jcs" - "github.com/lestrrat-go/jwx/v2/cert" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/sdk" ) const ( - // SystemMetadataAssertionID is the standard ID for system metadata assertions - SystemMetadataAssertionID = "system-metadata" - // SystemMetadataSchemaV1 defines the schema version for system metadata - SystemMetadataSchemaV1 = "system-metadata-v1" // kAssertionSignature is the JWT claim key for assertion signatures kAssertionSignature = "assertionSig" // kAssertionHash is the JWT claim key for assertion hashes kAssertionHash = "assertionHash" ) -// AssertionConfig defines an assertion to be included in the TDF during creation. -// -// AssertionConfig extends Assertion with a signing key, enabling creation -// of cryptographically signed assertions. The signing key is used during -// TDF creation but is not stored in the final TDF. -// -// Required fields: -// - ID: Unique identifier for the assertion -// - Type: The kind of assertion (BaseAssertion, HandlingAssertion) -// - Scope: What the assertion applies to (PayloadScope, TrustedDataObjScope) -// - AppliesToState: When the assertion is relevant (Encrypted, Unencrypted) -// - Statement: The assertion content and metadata -// -// Optional fields: -// - SigningKey: Custom signing key (defaults to DEK with HS256) -// -// Example: -// -// assertion := AssertionConfig{ -// ID: "retention-policy", -// Type: HandlingAssertion, -// Scope: PayloadScope, -// AppliesToState: Unencrypted, -// Statement: Statement{ -// Format: "json", -// Schema: "retention-v1", -// Value: `{"retain_days": 90, "auto_delete": true}`, -// }, -// } -type AssertionConfig struct { - ID string `validate:"required"` - Type AssertionType `validate:"required"` - Scope Scope `validate:"required"` - AppliesToState AppliesToState `validate:"required"` - Statement Statement - SigningKey AssertionKey -} - -// Assertion represents a cryptographically signed assertion in the TDF manifest. -// -// Assertions provide integrity verification and handling instructions that are -// cryptographically bound to the TDF. They cannot be modified or copied to -// another TDF without detection due to the cryptographic binding. -// -// The assertion structure includes: -// - Metadata: ID, type, scope, and state applicability -// - Statement: The actual assertion content in structured format -// - Binding: Cryptographic signature ensuring integrity -// -// Assertions are verified during TDF reading to ensure they haven't been -// tampered with since TDF creation. -type Assertion struct { - ID string `json:"id"` - Type AssertionType `json:"type"` - Scope Scope `json:"scope"` - AppliesToState AppliesToState `json:"appliesToState,omitempty"` - Statement Statement `json:"statement"` - Binding Binding `json:"binding,omitempty"` -} - -var errAssertionVerifyKeyFailure = errors.New("assertion: failed to verify with provided key") - -// Sign signs the assertion with the given hash and signature using the key. -// It returns an error if the signing fails. -// The assertion binding is updated with the method and the signature. -func (a *Assertion) Sign(hash, sig string, key AssertionKey) error { - tok := jwt.New() - if err := tok.Set(kAssertionHash, hash); err != nil { - return fmt.Errorf("failed to set assertion hash: %w", err) - } - if err := tok.Set(kAssertionSignature, sig); err != nil { - return fmt.Errorf("failed to set assertion signature: %w", err) - } - - // sign the hash and signature - signedTok, err := jwt.Sign(tok, jwt.WithKey(jwa.KeyAlgorithmFrom(key.Alg.String()), key.Key)) - if err != nil { - return fmt.Errorf("signing assertion failed: %w", err) - } - - // set the binding - a.Binding.Method = JWS.String() - a.Binding.Signature = string(signedTok) - - return nil -} - -// Verify checks the binding signature of the assertion and -// returns the hash and the signature. It returns an error if the verification fails. -func (a Assertion) Verify(key AssertionKey) (string, string, error) { - parseOptions := []jwt.ParseOption{ - jwt.WithKey(jwa.KeyAlgorithmFrom(key.Alg.String()), key.Key), - } - - // Try to get a key from the JWS protected header if it exists - if decodedSig, err := jws.Parse([]byte(a.Binding.Signature)); err == nil && len(decodedSig.Signatures()) > 0 { //nolint:nestif // many keys - sig := decodedSig.Signatures()[0] - // First, try to get a JWK from the header - if jwkKey := sig.ProtectedHeaders().JWK(); jwkKey != nil { - var rawKey interface{} - if err := jwkKey.Raw(&rawKey); err == nil { - parseOptions = append(parseOptions, jwt.WithKey(sig.ProtectedHeaders().Algorithm(), rawKey)) - } - } - // Also try to get a key from x5c (certificate chain) header - if x5cChain := sig.ProtectedHeaders().X509CertChain(); x5cChain != nil && x5cChain.Len() > 0 { - // Get the first certificate (the signing certificate) - certBytes, ok := x5cChain.Get(0) - if ok { - // Parse the certificate to extract the public key - x509Cert, err := cert.Parse(certBytes) - if err == nil && x509Cert != nil { - parseOptions = append(parseOptions, jwt.WithKey(sig.ProtectedHeaders().Algorithm(), x509Cert.PublicKey)) - } - } - } - } - - tok, err := jwt.Parse([]byte(a.Binding.Signature), parseOptions...) - if err != nil { - return "", "", fmt.Errorf("%w: %w", errAssertionVerifyKeyFailure, err) - } - hashClaim, found := tok.Get(kAssertionHash) - if !found { - return "", "", errors.New("hash claim not found") - } - hash, ok := hashClaim.(string) - if !ok { - return "", "", errors.New("hash claim is not a string") - } - - sigClaim, found := tok.Get(kAssertionSignature) - if !found { - return "", "", errors.New("signature claim not found") - } - sig, ok := sigClaim.(string) - if !ok { - return "", "", errors.New("signature claim is not a string") - } - return hash, sig, nil -} - -// GetHash returns the hash of the assertion in hex format. -func (a Assertion) GetHash() ([]byte, error) { - // Clear out the binding - a.Binding = Binding{} - - // Marshal the assertion to JSON - assertionJSON, err := json.Marshal(a) - if err != nil { - return nil, fmt.Errorf("json.Marshal failed: %w", err) - } - - // Unmarshal the JSON into a map to manipulate it - var jsonObject map[string]interface{} - if err := json.Unmarshal(assertionJSON, &jsonObject); err != nil { - return nil, fmt.Errorf("json.Unmarshal failed: %w", err) - } - - // Remove the binding key - delete(jsonObject, "binding") - - // Marshal the map back to JSON - assertionJSON, err = json.Marshal(jsonObject) - if err != nil { - return nil, fmt.Errorf("json.Marshal failed: %w", err) - } - - // Transform the JSON using JCS - transformedJSON, err := jcs.Transform(assertionJSON) - if err != nil { - return nil, fmt.Errorf("jcs.Transform failed: %w", err) - } - - return ocrypto.SHA256AsHex(transformedJSON), nil -} - -func (s *Statement) UnmarshalJSON(data []byte) error { - // Define a custom struct for deserialization - type Alias Statement - aux := &struct { - Value json.RawMessage `json:"value,omitempty"` - *Alias - }{ - Alias: (*Alias)(s), - } - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - // Attempt to decode Value as an object - var temp map[string]interface{} - if json.Unmarshal(aux.Value, &temp) == nil { - // Re-encode the object as a string and assign to Value - objAsString, err := json.Marshal(temp) - if err != nil { - return err - } - s.Value = string(objAsString) - } else { - // Assign raw string to Value - var str string - if err := json.Unmarshal(aux.Value, &str); err != nil { - return fmt.Errorf("value is neither a valid JSON object nor a string: %s", string(aux.Value)) - } - s.Value = str - } - - return nil -} - -// Statement includes information applying to the scope of the assertion. -// It could contain rights, handling instructions, or general metadata. -type Statement struct { - // Format describes the payload encoding format. (e.g. json) - Format string `json:"format,omitempty" validate:"required"` - // Schema describes the schema of the payload. (e.g. tdf) - Schema string `json:"schema,omitempty" validate:"required"` - // Value is the payload of the assertion. - Value string `json:"value,omitempty" validate:"required"` -} - -// Binding enforces cryptographic integrity of the assertion. -// So the can't be modified or copied to another tdf. -type Binding struct { - // Method used to bind the assertion. (e.g. jws) - Method string `json:"method,omitempty"` - // Signature of the assertion. - Signature string `json:"signature,omitempty"` -} - -// AssertionType represents the category of assertion being made. -// -// Different assertion types serve different purposes in TDF handling: -// - HandlingAssertion: Instructions for data processing, retention, deletion -// - BaseAssertion: General-purpose assertions including metadata, audit info -type AssertionType string - -const ( - // HandlingAssertion provides instructions for data handling and processing. - // Examples: retention policies, deletion schedules, processing requirements - HandlingAssertion AssertionType = "handling" - // BaseAssertion is a general-purpose assertion type for metadata and other content. - // Examples: audit information, system metadata, custom business logic - BaseAssertion AssertionType = "other" +// The assertion types below are aliases onto their [github.com/opentdf/platform/sdk] +// counterparts, which own the implementation. They are kept here so that +// existing importers of this experimental package continue to compile +// unchanged, and so that assertions produced here interoperate directly with +// the stable SDK. Prefer the sdk-scoped names in new code. +type ( + // AssertionConfig defines an assertion to be included in the TDF during + // creation. It extends [Assertion] with a signing key, which is used + // during TDF creation but is not stored in the final TDF. + // + // See [sdk.AssertionConfig]. + AssertionConfig = sdk.AssertionConfig + + // Assertion represents a cryptographically signed assertion in the TDF + // manifest. Assertions provide integrity verification and handling + // instructions that are cryptographically bound to the TDF, so they + // cannot be modified or copied to another TDF without detection. + // + // See [sdk.Assertion]. + Assertion = sdk.Assertion + + // Statement includes information applying to the scope of the assertion. + // It could contain rights, handling instructions, or general metadata. + // + // See [sdk.Statement]. + Statement = sdk.Statement + + // Binding enforces cryptographic integrity of the assertion, so it + // cannot be modified or copied to another TDF. + // + // See [sdk.Binding]. + Binding = sdk.Binding + + // AssertionType represents the category of assertion being made. + // + // See [sdk.AssertionType]. + AssertionType = sdk.AssertionType + + // Scope defines what component of the TDF the assertion applies to. + // + // See [sdk.Scope]. + Scope = sdk.Scope + + // AppliesToState indicates when the assertion is relevant in the TDF + // lifecycle: before decryption (Encrypted) or after (Unencrypted). + // + // See [sdk.AppliesToState]. + AppliesToState = sdk.AppliesToState + + // BindingMethod represents the cryptographic method used to bind + // assertions to the TDF. + // + // See [sdk.BindingMethod]. + BindingMethod = sdk.BindingMethod + + // AssertionKeyAlg represents the cryptographic algorithm for assertion + // signing keys. + // + // See [sdk.AssertionKeyAlg]. + AssertionKeyAlg = sdk.AssertionKeyAlg + + // AssertionKey represents a cryptographic key for signing and verifying + // assertions. For RS256 the Key is an RSA private key (or a + // [crypto.Signer] for hardware-backed keys); for HS256 it is the shared + // secret bytes. + // + // See [sdk.AssertionKey]. + AssertionKey = sdk.AssertionKey + + // AssertionVerificationKeys represents the verification keys for + // assertions, with an optional default for unlisted assertion IDs. + // + // See [sdk.AssertionVerificationKeys]. + AssertionVerificationKeys = sdk.AssertionVerificationKeys ) -// String returns the string representation of the assertion type. -func (at AssertionType) String() string { - return string(at) -} - -// Scope defines what component of the TDF the assertion applies to. -// -// Scope determines which part of the TDF structure the assertion governs: -// - TrustedDataObjScope: Assertion applies to the entire TDF object -// - PayloadScope: Assertion applies only to the encrypted payload data -type Scope string - const ( - // TrustedDataObjScope indicates the assertion applies to the complete TDF object. - // This includes manifest, key access objects, and payload. - TrustedDataObjScope Scope = "tdo" + // SystemMetadataAssertionID is the standard ID for system metadata assertions. + SystemMetadataAssertionID = sdk.SystemMetadataAssertionID + // SystemMetadataSchemaV1 defines the schema version for system metadata. + SystemMetadataSchemaV1 = sdk.SystemMetadataSchemaV1 + + // HandlingAssertion provides instructions for data handling and processing. + // Examples: retention policies, deletion schedules, processing requirements. + HandlingAssertion = sdk.HandlingAssertion + // BaseAssertion is a general-purpose assertion type for metadata and other + // content. Examples: audit information, system metadata, custom business logic. + BaseAssertion = sdk.BaseAssertion + + // TrustedDataObjScope indicates the assertion applies to the complete TDF + // object, including manifest, key access objects, and payload. + TrustedDataObjScope = sdk.TrustedDataObjScope // PayloadScope indicates the assertion applies only to the payload data. // This is the most common scope for data handling assertions. - PayloadScope Scope = "payload" -) - -// String returns the string representation of the scope. -func (s Scope) String() string { - return string(s) -} - -// AppliesToState indicates when the assertion is relevant in the TDF lifecycle. -// -// This determines whether the assertion should be processed before or after -// decryption, enabling different handling patterns: -// - Encrypted: Process before decryption (e.g., access logging) -// - Unencrypted: Process after decryption (e.g., content filtering) -type AppliesToState string - -const ( - // Encrypted means the assertion should be processed before payload decryption. - // Used for access control, audit logging, and pre-processing requirements. - Encrypted AppliesToState = "encrypted" - // Unencrypted means the assertion should be processed after payload decryption. - // Used for content analysis, post-processing, and data handling requirements. - Unencrypted AppliesToState = "unencrypted" -) - -// String returns the string representation of the applies to state. -func (ats AppliesToState) String() string { - return string(ats) -} + PayloadScope = sdk.PayloadScope -// BindingMethod represents the cryptographic method used to bind assertions to the TDF. -// -// The binding method ensures assertions cannot be modified or transferred -// to other TDFs without detection. -type BindingMethod string + // Encrypted means the assertion should be processed before payload + // decryption. Used for access control, audit logging, and pre-processing. + Encrypted = sdk.Encrypted + // Unencrypted means the assertion should be processed after payload + // decryption. Used for content analysis and post-processing. + Unencrypted = sdk.Unencrypted -const ( // JWS (JSON Web Signature) is the standard method for assertion binding. - // Uses JWT-based cryptographic signatures for tamper detection. - JWS BindingMethod = "jws" + JWS = sdk.JWS + + // AssertionKeyAlgRS256 uses RSA-SHA256 for assertion signatures. Suitable + // when assertions must be verified without access to the signing key. + AssertionKeyAlgRS256 = sdk.AssertionKeyAlgRS256 + // AssertionKeyAlgHS256 uses HMAC-SHA256 for assertion signatures. More + // efficient, and lets the TDF's DEK double as the signing key. + AssertionKeyAlgHS256 = sdk.AssertionKeyAlgHS256 ) - -// String returns the string representation of the binding method. -func (bm BindingMethod) String() string { - return string(bm) -} - -// AssertionKeyAlg represents the cryptographic algorithm for assertion signing keys. -// -// Different algorithms provide different security and compatibility characteristics: -// - RS256: RSA-based signatures, widely supported, good for public key scenarios -// - HS256: HMAC-based signatures, simpler, good for shared key scenarios -type AssertionKeyAlg string - -const ( - // AssertionKeyAlgRS256 uses RSA-SHA256 for assertion signatures. - // Suitable when assertions need to be verified by parties without access to signing keys. - AssertionKeyAlgRS256 AssertionKeyAlg = "RS256" - // AssertionKeyAlgHS256 uses HMAC-SHA256 for assertion signatures. - // More efficient, suitable when the same key used for TDF encryption can sign assertions. - AssertionKeyAlgHS256 AssertionKeyAlg = "HS256" -) - -// String returns the string representation of the algorithm. -func (a AssertionKeyAlg) String() string { - return string(a) -} - -// AssertionKey represents a cryptographic key for signing and verifying assertions. -// -// The key can be either RSA or HMAC-based depending on the algorithm: -// - RS256: Key should be an RSA private key (*rsa.PrivateKey or jwk.Key) -// - HS256: Key should be a byte slice containing the shared secret -// -// Example usage: -// -// // HMAC key using TDF's Data Encryption Key -// hmacKey := AssertionKey{ -// Alg: AssertionKeyAlgHS256, -// Key: dek, // 32-byte AES key -// } -// -// // RSA key for public key scenarios -// rsaKey := AssertionKey{ -// Alg: AssertionKeyAlgRS256, -// Key: privateKey, // *rsa.PrivateKey -// } -type AssertionKey struct { - // Alg specifies the cryptographic algorithm for this key - Alg AssertionKeyAlg - // Key contains the actual key material (type depends on algorithm). - // Can be raw key material or a crypto.Signer for hardware-backed keys (HSM/KMS). - Key interface{} -} - -// Algorithm returns the cryptographic algorithm of the key. -func (k AssertionKey) Algorithm() AssertionKeyAlg { - return k.Alg -} - -// IsEmpty returns true if the key has no algorithm or key material configured. -// Used to check if a default signing key should be used instead. -func (k AssertionKey) IsEmpty() bool { - return k.Key == nil && k.Alg == "" -} - -// AssertionVerificationKeys represents the verification keys for assertions. -type AssertionVerificationKeys struct { - // Default key to use if the key for the assertion ID is not found. - DefaultKey AssertionKey - // Map of assertion ID to key. - Keys map[string]AssertionKey -} - -// Get returns the key for the given assertion ID or the default key if the key is not found. -// If the default key is not set, it returns an empty key. -func (k AssertionVerificationKeys) Get(assertionID string) (AssertionKey, error) { - if key, ok := k.Keys[assertionID]; ok { - return key, nil - } - if k.DefaultKey.IsEmpty() { - return AssertionKey{}, nil - } - return k.DefaultKey, nil -} - -// IsEmpty returns true if the default key and the keys map are empty. -func (k AssertionVerificationKeys) IsEmpty() bool { - return k.DefaultKey.IsEmpty() && len(k.Keys) == 0 -} diff --git a/sdk/experimental/tdf/manifest.go b/sdk/experimental/tdf/manifest.go index 10f5eceb0f..e9268859eb 100644 --- a/sdk/experimental/tdf/manifest.go +++ b/sdk/experimental/tdf/manifest.go @@ -7,69 +7,83 @@ import ( "errors" "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/sdk" ) +// These are unchanged copies of what sdk defines unexported. They stay +// here only while this package still builds manifests itself; the +// delegation that removes their last callers is a follow-up. const ( kGMACPayloadLength = 16 kSplitKeyType = "split" kPolicyBindingAlg = "HS256" ) -type RootSignature struct { - Algorithm string `json:"alg"` - Signature string `json:"sig"` -} - -type IntegrityInformation struct { - RootSignature `json:"rootSignature"` - SegmentHashAlgorithm string `json:"segmentHashAlg"` - DefaultSegmentSize int64 `json:"segmentSizeDefault"` - DefaultEncryptedSegSize int64 `json:"encryptedSegmentSizeDefault"` - Segments []Segment `json:"segments"` -} - -type KeyAccess struct { - KeyType string `json:"type"` - KasURL string `json:"url"` - Protocol string `json:"protocol"` - WrappedKey string `json:"wrappedKey"` - PolicyBinding interface{} `json:"policyBinding"` - EncryptedMetadata string `json:"encryptedMetadata,omitempty"` - KID string `json:"kid,omitempty"` - SplitID string `json:"sid,omitempty"` - SchemaVersion string `json:"schemaVersion,omitempty"` - EphemeralPublicKey string `json:"ephemeralPublicKey,omitempty"` -} - -type Method struct { - Algorithm string `json:"algorithm"` - IV string `json:"iv"` - IsStreamable bool `json:"isStreamable"` -} - -type Payload struct { - Type string `json:"type"` - URL string `json:"url"` - Protocol string `json:"protocol"` - MimeType string `json:"mimeType"` - IsEncrypted bool `json:"isEncrypted"` - // IntegrityInformation IntegrityInformation `json:"integrityInformation"` -} - -type EncryptionInformation struct { - KeyAccessType string `json:"type"` - Policy string `json:"policy"` - KeyAccessObjs []KeyAccess `json:"keyAccess"` - Method Method `json:"method"` - IntegrityInformation `json:"integrityInformation"` -} +// The manifest types below are aliases onto their +// [github.com/opentdf/platform/sdk] counterparts, which own the definitions. +// They are kept here so that existing importers of this experimental package +// continue to compile unchanged, and so a manifest produced here can be +// handed to the stable SDK without conversion. Prefer the sdk-scoped names in +// new code. +type ( + // RootSignature is the signature over the concatenated segment hashes. + // + // See [sdk.RootSignature]. + RootSignature = sdk.RootSignature + + // IntegrityInformation describes segment layout and the hashes that + // protect the payload. + // + // See [sdk.IntegrityInformation]. + IntegrityInformation = sdk.IntegrityInformation + + // KeyAccess is one wrapped key share addressed to a single KAS. + // + // See [sdk.KeyAccess]. + KeyAccess = sdk.KeyAccess + + // Method describes the payload encryption algorithm and IV. + // + // See [sdk.Method]. + Method = sdk.Method + + // Payload describes the encrypted payload entry in the archive. + // + // See [sdk.Payload]. + Payload = sdk.Payload + + // EncryptionInformation carries the policy, key access objects, and + // integrity information for a TDF. + // + // See [sdk.EncryptionInformation]. + EncryptionInformation = sdk.EncryptionInformation + + // Manifest is the TDF manifest written to manifest.json. + // + // See [sdk.Manifest]. + Manifest = sdk.Manifest + + // Segment is one encrypted chunk of the payload plus its integrity hash. + // + // See [sdk.Segment]. + Segment = sdk.Segment + + // PolicyBinding is the HMAC binding a key share to the policy. + // + // See [sdk.PolicyBinding]. + PolicyBinding = sdk.PolicyBinding + + // EncryptedMetadata is the AES-GCM envelope for a key access object's + // opaque metadata. + // + // See [sdk.EncryptedMetadata]. + EncryptedMetadata = sdk.EncryptedMetadata +) -type Manifest struct { - EncryptionInformation `json:"encryptionInformation"` - Payload `json:"payload"` - Assertions []Assertion `json:"assertions,omitempty"` - TDFVersion string `json:"schemaVersion,omitempty"` -} +// Policy, PolicyBody, and PolicyAttribute are deliberately not aliased onto +// [sdk.PolicyObject]: the sdk type declares Body as an anonymous struct over +// an unexported element type, so it has no nameable equivalent for these +// three. Exporting those in sdk first would make the alias possible. type PolicyAttribute struct { Attribute string `json:"attribute"` @@ -89,20 +103,9 @@ type PolicyBody struct { Dissem []string `json:"dissem"` } -type Segment struct { - Hash string `json:"hash"` - Size int64 `json:"segmentSize"` - EncryptedSize int64 `json:"encryptedSegmentSize"` -} -type PolicyBinding struct { - Alg string `json:"alg"` - Hash string `json:"hash"` -} -type EncryptedMetadata struct { - Cipher string `json:"ciphertext"` - Iv string `json:"iv"` -} - +// calculateSignature is an unchanged copy of the sdk function of the +// same name, retained only until this package stops building manifests +// itself. func calculateSignature(data []byte, secret []byte, alg IntegrityAlgorithm, isLegacyTDF bool) (string, error) { if alg == HS256 { hmac := ocrypto.CalculateSHA256Hmac(secret, data)